Type-safe API clients for web-frontends
Many backend frameworks, independent of the language in use, can automatically generate OpenAPI specifications the server code. We can use this specification to build documentation and a user-interface for testing. But we can also use it to generate client code for our web-frontend. Here are some code-generators.
The pain of missing types
Once in my career, I joined a team that was working on rather large JavaScript based web-frontend. The fact that there are no type annotations in JavaScript made it very difficult to understand the project in detail. If complex objects are passed around between functions, I often had to trace the data back to the originating backend call. Then I have to call the backend myself in order to see what the data looks like and of course, I just got an example response. I could never be sure that there weren’t any optional fields missing in the response.
At some point, we used OpenAPI/Swagger to generate a UI, and you could see the actual data-structures on that page… So far so good.
Using TypeScript
In the next project, we decided to use TypeScript from the start. We defined a function for each endpoint, which in turn was using axios to access the backend. This function was typed with the proper response. Like in this example:
import axios, { AxiosResponse } from "axios";
interface TodoResponse {
id: number;
name: string;
description: string;
}
interface TodoRequest {
name: string;
description: string;
}
interface TodoListResponse {
todos: TodoResponse[];
}
export async function addTodo(todo: TodoRequest): Promise<TodoResponse> {
const response = await axios.post<TodoRequest, AxiosResponse<TodoResponse>>(
"/api/todos",
todo,
);
return response.data;
}
This approach worked pretty well, but there was one annoying issue: Every time the backend created or changed an endpoint, we had to handcraft the types for the backend response.
What if we could generate this code automatically?
OpenAPI to TypeScript
OpenAPI cannot only be used to create fancy, interactive documentation. It can also generate client code (even server code) for several programming languages. The official tool for this is the OpenAPI generator. While this tool is certainly mighty and versatile, it has a problem: It is written in Java.
Since the whole ecosystem of frontend builds is written in Node.js, it is easier and faster to use a Node.js based tool.
After some research, I came up with three candidates, that I found useful and that I will now present in more detail.
Avoiding vendor lock-in
Don’t fall for the temptation to use the generated in your components directly.
If you decide to use a different library, you will have a lot of work fixing
your code. I usually still write an addTodo
function like in the example
above. I still define the parameter and return-types for that function. Then I
call the generated client from this function.
Using the generated code is just a confirmation that my frontend models match the actual backend. The benefits:
- If you change the backend, you can regenerate your code and then adjust the
addTodo
function to transform the data to the frontend model. - If you decide to refactor you frontend model, you can use the refactoring functions of you IDE instead of having to look at 2500 compile errors.
- If you decide to use a different code-generator, you just need to adjust the code in the one folder.
Code generators
In the following examples, I use a very small OpenAPI-spec as source. It
contains a single endpoint that can receive a JSON like { "password": "test" }
in the request body and responds with something like
{"encryptedPassword": "dqkdnjqwejnedlqwe, "adminPasswordConfigKey": "some.key" }
{
"openapi": "3.0.3",
"info": { "title": "Gachou Backend", "version": "0.0.1" },
"paths": {
"/api/encryptPassword": {
"post": {
"operationId": "encryptPassword",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PasswordEncryptionRequest"
}
}
}
},
"responses": {
"200": {
"description": "On success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/EncryptedPasswordResponse"
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"EncryptedPasswordResponse": {
"required": ["encryptedPassword", "adminPasswordConfigKey"],
"type": "object",
"properties": {
"encryptedPassword": { "type": "string", "nullable": false },
"adminPasswordConfigKey": { "type": "string", "nullable": false }
}
},
"PasswordEncryptionRequest": {
"required": ["password"],
"type": "object",
"properties": { "password": { "type": "string", "nullable": false } }
}
}
}
}
The source of the examples can be found under https://github.com/nknapp/typesafe-api-client-examples
openapi-client-axios
The library openapi-client-axios was my preferred choice until recently.
The pros
It uses and exposes axios to run http-requests, which mean that you can use axios-features, like “interceptors”. This allows you to do perform custom error handling and authentication at a single point in your code.
The cons
The downside of this library is, that it is a one-man project. There is only a single author on npm and not issues get fixed very quickly, including my issue from Dec 2, 2020.
The library currently does not work with vite, because of this issue.
Using openapi-client-axios
To generate types you can install openapi-client-axios-typegen
, which allows
you to generate typings by running
npx typegen ./openapi.json > client.d.ts
After that, you can use those types:
import OpenAPIClientAxios, {OpenAPIV3} from 'openapi-client-axios';
import spec from './openapi.json'
import {Client} from "./client";
const api = new OpenAPIClientAxios({definition: spec as OpenAPIV3.Document});
const client = api.initSync<Client>();
client
is now has functions for all operation-ids found in the OpenAPI-spec,
and with because of generated typings, you get type-checks and auto-completion:
oazapfts
The library oazapfts is currently my favorite, and I am using in my private project Gachou.
The pros
- Multiple collaborators (four, as of today) have been publishing this project on npm. This looks like less risk of being abandoned, but it is far away from other open-source projects that have large teams of maintainers.
- It works with
vite
. - It generates tree-shakable code.
The cons
I don’t see any real drawbacks at the moment. The library uses fetch
directly,
so you cannot use axios-features. But you can configure global headers via the
exposed defaults
-objects of the module. You can even specify a custom fetch
function, which allows you to wrap the browser’s fetch
with your own wrapper.
You cannot have multiple instances of the same api using different defaults, though.
I will show one other slight con in a moment.
Usage
After installing the module, you can run
npx oazapfts ./openapi.json oazapfts-client.ts
The client can simply be used:
In this example the ok()
-method is a wrapper that throws an error if the
response is anything other than an HTTP-2xx response code.
The slight drawback of this library is that I cannot name my function
encryptPassword
, because that is already the operation id and therefore the
name of the generated function. (Yes, I could use import ... as
to map the
function name, but I am lazy and the IDE adds the import for me, so I don’t want
to do a lot of work changing it.)
You can globally set request-headers
import { defaults } from "./oazapfts-client";
defaults.headers = {
authorization: "Bearer myToken",
};
openapi-typescript-codegen
I have come across this library when looking for TypeScript generators for the
first time. I abandoned it then, because we were usually using axios
and
openapi-typescript-codegen
didn’t support it back then
(issue#400).
This issue seems to be resolved now, so if you like, try it out.
The pros
- Can generate code for “fetch”, “xhr”, “node”, “axios” and “angular”
- Output can be tweaked by some cli options
The cons
- The library seems to be maintained by a single person only.
- Current version is 0.23.0, which means the maintainer does not consider the API stable yet.
Apart from that, I cannot say much, because I haven’t used the library yet.
Usage
Running the command
openapi -i ./openapi.json -o __generated__
will create multiple files in the __generated
-directory. The entrypoint is
exposed in the __generated__/index.ts
file. You can use it like
import { DefaultService } from "./__generated__";
async function encryptPassword() {
const response = await DefaultService.encryptPassword({ password: "test" });
return response.encryptedPassword;
}
All in all, the generated code is less compact than the code of oazapfts
, but
it is certainly worth a look, if you are looking for such a tool and the other
two don’t fit your needs.
Conclusion
There are several tools available to convert OpenAPI specifications to Typescript clients. If you use such a tool, you will get auto-completion and type-checks for request- and response-body properties, for query-parameters and for operation-ids.
Be careful not to lock yourself in to one tool, but rather add another layer of indirection. It will help you with changes later-on.
I have presented three of those tools here and my favorite currently is
oazapfts
, It generates compact, tree-shakable code and while still being
configurable. It is not possible to create multiple instances of a service, but
I also cannot imagine a use-case for such a feature.