DEV Community

Brady Holt for YNAB

Posted on

How we use OpenAPI / Swagger for the YNAB API

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.

Endpoint

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:

Endpoint Try-it Now

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:

IntelliSense

Develop-time errors (a.k.a. red squiggles) so a developer can see when they have accessed a field that does not exist:

Red squiggles

Enums selection so a developer can easily select from a list of supported values:

Enums

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)

Collapse
 
ozonep profile image
Ivan

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 :)

Collapse
 
bradymholt profile image
Brady Holt • Edited

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!