OpenAPI Specification | ➡ | ✅ Tests ✅ Documentation ✅ Client Libraries |
---|
YNAB combines software with 4 simple rules to help our users gain control their money. Back in 2018 we released an API to help our community build things to connect their budget to other apps and services.
We built the YNAB API using the OpenAPI Specification and Swagger tooling. In retrospect we think it was a good decision and has provided many benefits. This post gives a landscape of our setup and how the tooling works.
The specification file
Everything starts with our OpenAPI Specification file: https://api.youneedabudget.com/papi/spec-v1-swagger.json. This is a fairly simple JSON file that we edit when anything changes on the API.
This file defines the endpoints and the shape of request and response data.
With this spec file in place, we get access to some great tooling.
Tests
We use a Ruby gem called Apivore to test our actual API implementation against our OpenAPI spec. When our tests run, they exercise all the spec defined endpoints and ensure they are present and work as expected. With Apivore, you have a validate_all_paths
method, which does a simple validation to ensure endpoints are available and return the expected statuses. But, you can also exercise the API by passing certain data and Apivore will ensure the shape of the data responses match your spec. So, if your spec says an endpoint returns an array of sharks: [{"id": 1, "name": "megamouth"},{"id": 2, "name": "hammerhead"}]
but a particular request returns a single shark: {"id": 1, "name": "megamouth"}
a test will fail. Another example: If you were to change the name of a field in the JSON response, a test would fail.
Examples
Here, one of our tests ensures that requesting a non-existent budget at /budgets/:id
returns a 404.
it 'response with 404 for a non-existent budget' do
expect(subject).to validate(
:get, '/budgets/{budget_id}', 404, params.merge({
'budget_id' => SecureRandom.uuid
})
)
end
Here, we ensure the /budgets/:id/payees/:id
endpoint returns a payee, when requested, and also that the shape of that payee conforms to our spec:
it 'returns a single payee' do
expect(subject).to validate(
:get, '/budgets/{budget_id}/payees/{payee_id}', 200, params
)
end
The ability to use a tool like Apivore against our spec is a huge win because we get automatic testing. Having this alone would be a case for using an OpenAPI spec.
Documentation
Using Swagger UI we are able to automatically generate our documentation page just by pointing to our spec. We did make some customizations to suit our preferences but any changes to our spec file are automatically reflected on that page.
For example, the documentation for GET /budgets/:id
shows the query parameters that can be passed, expected response status code, and the shape of the response data.
Swagger UI also has the ability to actually use the API on the page itself which is a great help for developers wanting to test something or quickly see the API responses.
For example, this is what the page looks like when requesting a specific payee:
Client Libraries
Arguably, Swagger tooling is most known for Swagger Codegen, which generates client libraries for interfacing with an API.
We use Codegen to generate our JavaScript client and our Ruby client. And others in our community use it to build clients for other languages.
One of the nice things about Codegen is the ability to customize the generated client with the use of templates. For example, we specify a number of templates to override the defaults on our JavaScript client. This allows us to customize things to our suit our preferences.
TypeScript Definitions
Our JavaScript client uses the typescript-fetch
generator which, along with generating a JavaScript client usable from both Node.js and the browser, generates TypeScript definition files. This is really useful because developers who are using TypeScript tooling (like VS Code) can get develop-time support.
Examples
IntelliSense support so a developer can clearly see the available fields:
Develop-time errors (a.k.a. red squiggles) so a developer can see when they have accessed a field that does not exist:
Enums selection so a developer can easily select from a list of supported values:
All of this support is coming directly from the original OpenAPI specification file!
Overall Experience
We've been pleased with our usage of an OpenAPI specification and Swagger tooling to build out our API and the ecosystem around it. Of course, there have been a few bumps along the way and we still would like to tweak some things but the amount of benefit this tooling brings is significant and allows us to ship things more rapidly.
Top comments (2)
Great article, thanks!
I love API-first approach, and always try to use it in my projects.
Also I appreciate that I can take my YAML and generate both client and server code from it.
But I don't like existing generators, usually they produce code with many abstractions: often server is being generated on-the-fly from YAML. So the whole server is just one small JS file. Or generator uses classes, which doesn't help readability. Or other things. It makes sense, they tend to support all the languages/frameworks.
Maybe I'm the only one, but I want generated code to be just like the one I would create on my own from scratch, so then it's easy to read & maintain, and use.
I even had to create my own implementation for generating server from OpenAPI YAML spec:
github.com/ozonep/openapi-fastify-...
But anyway - it's hard to overestimate importance of OpenAPI in API design process :)
Thanks for the comment Ivan! We use Codegen for our clients but haven't used any of the tooling to generate the actual server implementation. We implemented that ourselves, primarily for the reasons you mentioned. I'll check out your server generator!