Hyperproductive API clients with FastAPI using OpenAPI Generator

3 minute read

OpenAPI-Generator is a spectacular tool built on top of OpenAPI schemas that can generate API clients from OpenAPI schemas. Coupled with the automatically generated OpenAPI schema in FastAPI, it’s a formidable paradigm to learn. I’ve been using this pattern in some of my recent projects and it’s made me very productive.

A working example of this blog post can be found in this bare-bones Github project.

If you are looking for a real world example using this pattern, you can look at my FastAPI-Starter project, which is a Cookiecutter project that uses this pattern and tons of other helpful features.

Requirements

You need a FastAPI backend app. Although, if you already have an OpenAPI spec some other way, this article can still be helpful.

I’ll create a Typescript client for front-end here. However, other clients, including Android and iOS are also possible — or any other programming language you can think of is likely supported by OpenAPI Generator.

With Typescript, there is more than one type of client available. I choose to use typescript-axios, which pulls in an external dependency on axios.

Prerequisites for OpenAPI Generator

Simplify operation names

First of all, you want to simplify the operation names in the OpenAPI spec generated by FastAPI. This is, strictly speaking, not necessary but if you don’t do this the generated code will use function names like getItemByIdItemsIdGet instead of something simple like getItems.

One reason for skipping this step might be that you have the same Python function names (across your whole codebase, not just one module) and you don’t want to refactor your code.

To simplify operation names, add this snippet to your code and make sure to call the use_route_names_as_operation_ids AFTER you register all routes in FastAPI.

from fastapi.routing import APIRoute

def use_route_names_as_operation_ids(app: FastAPI) -> None:
    """
    Simplify operation IDs so that generated API clients have simpler function
    names.

    Should be called only after all routes have been added.
    """
    route_names = set()
    for route in app.routes:
        if isinstance(route, APIRoute):
            if route.name in route_names:
                raise Exception("Route function names should be unique")
            route.operation_id = route.name
            route_names.add(route.name)

use_route_names_as_operation_ids(app)

Install Java

OpenAPI generator uses Java under the hood and you’ll need to install it on your machine. If you already have it, you can proceed to the next step.

Add CLI wrapper to your project dependencies

yarn add -D @openapitools/openapi-generator-cli@latest

At the same time, you can also add axios to your project if you don’t have it already:

yarn add axios

As of writing this, OpenAPI Generator 5.4.0 doesn’t work with the schema generated by FastAPI (external issue), so I used 5.3.1.

yarn openapi-generator-cli version-manager set 5.3.1

Generate Typescript code

First download the OpenAPI schema to a temporary location:

curl 'http://localhost:8000/openapi.json' -o /tmp/openapi.json

Finally, you can create Typescript bindings using this command:

yarn openapi-generator-cli generate \
    -i /tmp/openapi.json \
    -g typescript-axios \
    -o generated \
    -p withSeparateModelsAndApi=true,apiPackage=api,modelPackage=models,useSingleRequestParameter=true
  • The first time you run openapi-generator-cli, it will create an openapitools.json configuration file, that should be committed to version control.
  • This command will read /tmp/openapi.json (-i switch) and output code to generated directory (-o switch) using the typescript-axios generator (-g switch)
  • The -p switch specifies some options, the documentation for the parameters specified is here.

Use generated bindings

I create an env.ts file like this:

import { UsersApi, ItemsApi } from "./generated/api";
import { Configuration } from "./generated/configuration";

const readAccessToken = async (): Promise<string> => {
  return localStorage.getItem("token") || "";
};

const basePath: string = "http://localhost:8000";

const apiConfig: Configuration = new Configuration({
  basePath,
  // accessToken points to a function that is called for every request. It's not needed for public APIs
  accessToken: readAccessToken,
});

export const usersApi: UsersApi = new UsersApi(apiConfig);
export const itemsApi: ItemsAPI = new ItemsApi(apiConfig);

This module exports two ready-to-use APIs: usersApi and itemsApi.

If our usersApi has a getUsers function, we’d use it like this:

const users = await usersApi.getUsers();

console.log(users.data);

That’s it!

Making things easy(ier)

For convenience, I add a genapi command to my package.json in the scripts section:

{
    "scripts": {
        "genapi": "openapi-generator-cli generate -i <(curl -s 'http://localhost:8000/openapi.json') -g typescript-axios -o generated -p withSeparateModelsAndApi=true,apiPackage=api,modelPackage=models,useSingleRequestParameter=true"
    }
}

Now I just have to run yarn genapi in the front-end code directory and voilà, my auto-generated code is ready to use.