We have all been there when you look at the documentation and write a client that fetches some data, only to then find that the response doesn’t quite line up with the documentation. It’s this shared experience that drove us in the engineering team at SAPI to think differently.
Before writing any code, we document our schemas in an OpenAPI v3 spec, including our endpoints with request and response formats and the model classes used within them. This ensures we are very clear with our domain before we can even write any of the applications.
As a result, we can only create endpoints which have been documented in the schema (and therefore our public API docs), and when we are implementing these endpoints, we have typed class definitions ready to use in our code for them. This enforces that we can’t change any existing fields, or add any new ones, without it being in the schema.
Having all schemas defined before writing code means that we publicly document exactly what our API does before any clients interact with it. It’s a product manager’s dream!
The openapi-typescript CLI runs after any changes are made to the OpenAPI spec, which generates type definitions for our code to use for all models and endpoints. This enforces that our requests and responses always match the same types that we’ve documented.
We use Redocly to generate our public API docs which provide a further configuration in the OpenAPI spec. This includes things such as Tag Groups (to group endpoints together), Logos, and server URLs. We have an automated process which deploys our public API docs on every deployment.
Our testing structure is focused on the full behavioural use cases, which checks all the public-facing API endpoints. This test server is created by registering all endpoints from the OpenAPI schema in a Fastify instance, which also ensures it behaves exactly the same as our production server.
We came across a problem where we wanted to version our endpoints to be more flexible for future requests. Having a
/v1/ parameter in the path was easily done in the
fastify-openapi-glue plugin, but being able to accept different versioned requests not so much.
We had thought about having a
X-Version header in requests that contains the version of the request/response, with part of the controller reading this property in order to handle the request. This leads to the problem of how our API docs would be structured and built for each version.
Our API can contain some partner-specific endpoints and schemas, which means we need to be able to build custom API docs for them. This is where we diverge from what the libraries we are using support and have to write our own custom code.
fastify-openapi-glue plugin has a lot of weekly downloads and active releases from the maintainers, there is not a lot of documentation or community support around how to best solve the problems you have with the library.
We had a few problems where we were trying to do something that wasn’t supported out-of-the-box by the library. This meant taking the time to look into how we can solve these problems, including forking the library to add our own additions to it, with a company perspective of contributing the changes back into the open-source community.
Write Once, Use Everywhere
Because our schema is the source of truth for all our endpoints and models, we write our definitions once and then use them everywhere in the codebase. This keeps our code very clean because we’re just wiring up the definitions that we have in the OpenAPI schema, and it’s all type-checked to ensure we haven’t diverged anywhere.
We use the OpenAPI-TypeScript library to generate TypeScript definitions for all the schemas and endpoints. This means that we never make changes to the models directly, they instead have to be changed through the OpenAPI spec, and then the models get re-generated, which enforces that the OpenAPI spec is the source of truth for our entire API.
Automated Build Process
The OpenAPI schema is baked into our build process, every change builds a new set of public API docs which is then uploaded to the public website. We also verify in our tests that there are no breaking changes to any existing models or endpoints.
In the long term, we can also build automated client libraries in any language using the OpenAPI Generator, which can build an SDK in all languages used by our clients to make the integration even more simple and easy.
We will continue to advocate for a design-driven approach in the industry, proving that having an upfront schema and clear motivation when there are changes made ensures a clean product-driven development cycle.
Having a design-driven API means our development team is always aligned with the product, and we never make any unintentional changes to the API which would affect our clients. We’ve found that approaching the documentation first has changed the way that we’ve structured our endpoints and models, and we believe this is for the better.
We’re currently on the lookout for engineers to join the team. If you enjoyed this blog and are interested in how we work at SAPI, reach out to me or check out our open positions at https://sapi.com/careers.