API Code-first: How to Generate OpenAPI from Code

An update on the API Code first or Design first debate, with some modern tools making a slightly nicer version of the Code first workflow, and better modern design first tooling.

API Code-first: How to Generate OpenAPI from Code

We've posted about API Code-first vs API Design-first a fair few times over the years, but this article aims to be an updated look at some of the current day tooling which makes both workflows nicer to handle.

🫶
Thank you to Bump.sh for sponsoring this post, and giving me time to write down some updated thoughts on this topic.

API Code-first is the art of building an API and then popping some annotations or metadata in there to pop out API documentation in a API description format like OpenAPI. Is that how you want to keep doing things? Maybe, there's some tooling out that's made it a lot easier. There's also a few tools knocking around mean you might not need to.

For those of you used to the API Code-first here are the three main workflows you should be thinking about going forwards. If you've been documenting your APIs entirely manually with some sort of content management system, wiki, or Word Document, then these ideas might save you from that nightmare.

Annotations

The classic approach to the API Code-first workflow is to use code comments or some other form of annotations as extensions or plugins to write OpenAPI mixed in with the code it's describing.

Here's how these annotations look in Go:

// @title           Swagger Example API
// @version         1.0
// @description     This is a sample server celler server.
// @termsOfService  http://swagger.io/terms/

// @contact.name   API Support
// @contact.url    http://www.swagger.io/support
// @contact.email  support@swagger.io

// @host      localhost:8080
// @BasePath  /api/v1

// @securityDefinitions.basic  BasicAuth

func main() {
    r := gin.Default()

    c := controller.NewController()

    v1 := r.Group("/api/v1")
    {
        accounts := v1.Group("/accounts")
        {
            accounts.GET(":id", c.ShowAccount)
            accounts.GET("", c.ListAccounts)
            accounts.POST("", c.AddAccount)
        }
    //...
    }
}

Then the schema level descriptions are mixed in with the code responsible for outputting resources like this:

type Account struct {
    ID   int    `json:"id" example:"1"`
    Name string `json:"name" example:"account name"`
}

Once the API, endpoints, and resources have all the appropriate annotations there is usually some sort of command you can run to get an OpenAPI document out of it, and that machine-readable document can be used to deploy documentation to Bump.sh or wherever your API documentation lives.

swag init --outputTypes yaml

bump deploy swagger.yaml \
  --doc my-documentation-name \
  --token my-documentation-token

This approach has been popular for years, with the main selling point being the idea that keeping OpenAPI metadata near the code will hopefully mean developers keep it up to date as they work on the code. This is not always the case, which is one of a few reasons this practice is dying out.

The other is that many of the annotation tools are stuck on older less useful versions of OpenAPI, namely v2.0 instead of v3.0, or the latest and greatest: v3.1.

Depending on your language and framework choices you may or may not have an option for working with modern OpenAPI, but the lack of modern tooling has been a driving force in people giving up on this approach and looking for alternative workflows. Let's have a look at some others.

OpenAPI-aware Frameworks

There's a new breed of API-centric backend application frameworks popping up which take an exciting approach. Instead of asking you to tack the annotations in around the existing codebase, the frameworks simply produce OpenAPI for you from the actual code you're writing.

Your application is already declaring routes, defining parameters and incoming validation logic, and helping serialize output. It makes a lot of sense for the framework to help produce this machine readable format for you, from the code you're already writing.

There are not as many tools that work this way, but this is likely to be a trend that continues as OpenAPI becomes the dominant API description format.

Just like annotations you can usually run a command to extract the OpenAPI document, or you can run the web server and pull it down over HTTP.

$ go run .

$ bump deploy http://127.0.0.1:8888/openapi.yaml \
  --doc my-documentation-name \
  --token my-documentation-token

Traffic Sniffing

If there's no annotations approach, and you have an existing codebase which cannot be rebuilt with one of these OpenAPI-aware application frameworks, there is another powerful option: sniffing web traffic.

There's a whole category of tools popping up, which refer to this functionality as "Recording" or "Learning".

Basically you run an instance of your API somewhere (could be local, test, staging, or even production) and put as much web traffic through it as possible. It will then learn how all the requests and responses look, and produce the best composite OpenAPI that it possibly can.

Turn HTTP Traffic into OpenAPI with Optic
Capture real HTTP traffic from production or anywhere else, and create OpenAPI from it, for documentation, mocks, SDKs, or contract testing.

You can usually run some sort of proxy to do this, but there are other ways, like using rspec-openapi for Ruby on Rails users. This tool will sniff all the integration tests coming through the test suite to the API and use that to construct a rudimentary OpenAPI description.

Code-first usually needs enhancing

Whether you're generating from annotations, the framework, or HTTP traffic, there's a strong chance that you'll need to put some work in to improve the quality of that OpenAPI. It's going to be missing long form descriptions, the sort of content that tech writers often produce, and depending on the tool used it's probably going to be missing examples too. In order to improve this you can use OpenAPI Overlays to enrich the generated OpenAPI with your own logic, and avoid it being overridden the next time OpenAPI is generated.

Move to API Design-first

With so many of the annotations approaches being outdated, and people usually unable to rebuild an entire codebase to use a framework that happens to emit OpenAPI, a lot of people have given up on the whole code-first approach.

This is not just an opinion. Searching around the Go community for code-first tooling, most of the "How do I do Code-first in Go" search results show people talking about how they moved to API Design-first and massively prefer the approach.

The main idea is that instead of writing loads of code and sprinkling in some annotations later to create docs, you create the OpenAPI before writing any code at all. This is usually in the form of JSON/YAML, but that does not need to be written by hand. There are lots of visual editors to help you build this all up through buttons and forms, with an increasing amount of intelligence to create things. If you're using VS Code then Copilot is actually incredibly good.

Once you have the OpenAPI document you can leverage it at every step of the API lifecycle, producing mock APIs for clients to test assumptions with, produce client libraries without writing any code, make really effective contract testing, even generate backend code to get the application teams started once the contract is all signed off. API Design-first is a bit more work up front with a massive payoff in productivity going forwards and forever.

The main concern with API Design-first is "drift", where the code and schema diverge over time. The annotations approach only pretended to solve this problem, confusing proximity with accuracy. The comments above code could still completely fail to accurately describe the code below, but nobody would ever notice until a user complained about it.

The OpenAPI-aware Framework approach does solve this by making the code a single source of truth, but the Design-first approach can be used to make any framework OpenAPI aware, with a source of truth that exists before the code, and continues to be useful after the code is built.

Server-side validation with OpenAPI can avoid the need to write lots of request validation, using middleware to compare incoming HTTP requests against the contract defined in the OpenAPI description and automatically return validation errors instead of having to build that all our yourself.

Responses can be validated using any existing test suite, with all popular testing frameworks supporting OpenAPI by extension.

Years ago the API Design-first workflow was a rough approach, but thankfully a whole bunch of tooling developers spent those years making things excellent, and now it's easier than ever. Bump.sh adds to that legacy by adding amazing change detection, helping check the OpenAPI in your git repository for changes that would be breaking for end users, letting you know in the pull request when there's a problem, providing beyond a shadow of a doubt that having your OpenAPI as a source of truth in a git repository along with your source code is not only handy, but probably the best way to go for many teams.

Support APIs You Won't Hate

When you become an member, you'll get access to members-only content while directly supporting our work. Your support helps us to keep making resources for the API community.

Become a member today