Fully typed Web Apps with OpenAPI (Part 1)

Guillaume Renard
7 min readMar 6, 2024

--

Meet OpenAPI generator, the unsung hero for fully typed APIs.

A superhero with a cape and a mask on his eyes, with colors light green, white, and dark gray, turning his back on the camera, who is a developer and not very muscular.
Image generated with AI (Microsoft Copilot)

Challenges & solutions

React legend Kent C. Dodds wrote about the challenges and benefits of fully typed web apps in the Epic Web series. If you haven’t read it yet, you should give it a go, it’s well worth it.

One of the biggest pain points in web development is making network requests and processing their response. In the past years, new technologies have emerged to make this task easier and safer.

For example, if you use TypeScript in both your frontend and backend, then tRPC is a great candidate to share your types between the two. tRPC has been popularized by the T3 Stack created by the famous Youtuber Theo.

Zod is another library that is commonly used with tRPC, which you can also use on its own. Zod allows you to define schemas to validate your data and infer types. This is great for validating the input and output of your APIs and keeping your app fully typed.

What about OpenAPI?

All of this is great, but if you’ve worked in a medium or a large company, you know that the backend and frontend are often developed by different developers, using different technologies. So forget tRPC. Maybe you could use Zod. But chances are your company already uses OpenAPI (formerly known as Swagger) to document its services.

Yes, OpenAPI documentation looks great and can be understood by all the stakeholders, whether they are technical people or not. But OpenAPI can do much more. Yet, only a few companies are making the most of it.

How many frontend developers out there are still translating OpenAPI docs to code by hand? Not only that is tedious, but it is also very error-prone. Surely there must be a better way!

Introducing OpenAPI generator

Enter the magical world of OpenAPI generator! Generators allow you to generate a variety of contents from your OpenAPI documents:

In this article, we’ll focus on client generators.

Example

Let’s use a concrete example. Imagine that you are the owner of an online pet store and that your service is described in this OpenAPI document: petstore.yaml. If you’ve read some OpenAPI documentation before, you won’t find it very original… But it is still a good example.

This document defines a few operations: add a pet, update its details, create orders, manage users, etc… Even for the non-initiated, the OpenAPI format is fairly easy to read and understand. That’s why developers and stakeholders like it so much.

For example, here is the definition of the endpoint used to add a pet to the store:

paths:
/pet:
post:
tags:
- pet
summary: Add a new pet to the store
description: ''
operationId: addPet
responses:
'200':
description: successful operation
content:
application/xml:
schema:
$ref: '#/components/schemas/Pet'
application/json:
schema:
$ref: '#/components/schemas/Pet'
'405':
description: Invalid input
security:
- petstore_auth:
- 'write:pets'
- 'read:pets'
requestBody:
$ref: '#/components/requestBodies/Pet'

and the definition of the data it refers to:

components:
requestBodies:
Pet:
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
application/xml:
schema:
$ref: '#/components/schemas/Pet'
description: Pet object that needs to be added to the store
required: true
schemas:
Pet:
title: a Pet
description: A pet for sale in the pet store
type: object
required:
- name
- photoUrls
properties:
id:
type: integer
format: int64
category:
$ref: '#/components/schemas/Category'
name:
type: string
example: doggie
photoUrls:
type: array
xml:
name: photoUrl
wrapped: true
items:
type: string
# more...

As a frontend developer, at this point, you could roll up your sleeves, grab a coffee, and start translating this document into code. With a fair amount of copy/pasting, you should get the job done in a few hours… not to mention the cost to your sanity and the few mistakes nobody spotted on the way.

Or you could use a generator.

From documentation to Code!

Let’s start with the unpleasant part… OpenAPI generator is implemented in Java (🤷), so you first need to install Java on your machine. But beware, there is a catch… The latest OpenAPI generator requires Java 11, which only comes with the full Java Development Kit (JDK) (160MB+) (Oracle stopped shipping the lightweight Java Runtime (JRE) at version 8 (🤷🤷).

Once you’re done, you can either stick with Java or use your favorite package manager to run the generator. My preferred option is to use a package manager, as it will allow us to integrate the generator into our existing projects later on.

The first step is to install the CLI package:

npm install @openapitools/openapi-generator-cli -g

Then, run it on the downloaded API, with the chosen generator. Let’s say our project uses Axios and TypeScript. We can use the following command line to generate the code into the petstore folder:

npx @openapitools/openapi-generator-cli generate -i petstore.yaml -g typescript-axios -o petstore

And if you don’t like Axios or Typescript (🤔), there are other generators available, see the complete list here.

The generated petstore folder contains a bunch of files:

.openapi-generator
.gitignore
.npmignore
.openapi-generator-ignore
api.ts
base.ts
common.ts
configuration.ts
git_push.sh
index.ts

The interesting part is located inside api.ts. The other files can be ignored for now (*ignore and .sh files aren’t useful unless you want to publish the folder to its own repo, and the other files only contain utility code).

If you open api.ts in your editor, you’ll see plenty of well-documented TypeScript interfaces, such as:

/**
* A pet for sale in the pet store
* @export
* @interface Pet
*/
export interface Pet {
/**
*
* @type {number}
* @memberof Pet
*/
'id'?: number;
/**
*
* @type {Category}
* @memberof Pet
*/
'category'?: Category;
/**
*
* @type {string}
* @memberof Pet
*/
'name': string;

// more...
}

Pretty neat! That’s a lot of weight taken away from the developer’s shoulders. And the quality of the output is probably greater than what you would have achieved by hand.

But the best part is yet to come. If you scroll further down that file, you’ll find fully implemented methods that you can use to call your endpoints:

 /**
* PetApi - object-oriented interface
* @export
* @class PetApi
* @extends {BaseAPI}
*/
export class PetApi extends BaseAPI {
/**
*
* @summary Add a new pet to the store
* @param {Pet} pet Pet object that needs to be added to the store
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof PetApi
*/
public addPet(pet: Pet, options?: RawAxiosRequestConfig) {
return PetApiFp(this.configuration).addPet(pet, options).then((request) => request(this.axios, this.basePath));
}

/**
*
* @summary Deletes a pet
* @param {number} petId Pet id to delete
* @param {string} [apiKey]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof PetApi
*/
public deletePet(petId: number, apiKey?: string, options?: RawAxiosRequestConfig) {
return PetApiFp(this.configuration).deletePet(petId, apiKey, options).then((request) => request(this.axios, this.basePath));
}

/**
* Multiple status values can be provided with comma separated strings
* @summary Finds Pets by status
* @param {Array<FindPetsByStatusStatusEnum>} status Status values that need to be considered for filter
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof PetApi
*/
public findPetsByStatus(status: Array<FindPetsByStatusStatusEnum>, options?: RawAxiosRequestConfig) {
return PetApiFp(this.configuration).findPetsByStatus(status, options).then((request) => request(this.axios, this.basePath));
}

// more...
}

Yep. Fully typed APIs. All of this was generated from a single OpenAPI file 🤯

Would you like to add a Pet to the store? No problem:

const added = await new PetApi().addPet({
name: 'Pikachu',
photoUrls: ['https://example.com/pikachu.jpg'],
category: {
id: 123,
name: 'Pokemon'
},
tags: [{
id: 123,
name: 'Electric'
}],
});

If you dig into the generated code, you’ll see that it takes care of validating the parameters, setting the right verb and headers, serializing the data if needed, sending the request with Axios, and returning the result, complete with the expected type.

And in case the API or the server required some configuration, or you wanted to use your own Axios instance for example, the generated code has got you covered too:

const api = new PetApi(
new Configuration({ accessToken: "eyJ0eXAiO", apiKey: "key-abc" }),
"https://api.example.com",
myAxios
);

You’re welcome.

OpenAPI in the real world

The quality of the generated API will depend on the quality of your documentation. The petstore.yaml file used in this example is good, if not perfect. Yours probably won’t be as good.

However, the best way to improve your OpenAPI documentation is to start using it to generate code. This will allow you to identify gaps and fix them until you’re happy with the output. By experience, I have seen great improvements to the OpenAPI documentation once developers started using it for code generation, leading to greater adoption (not to say addiction) on both the backend and frontend sides.

Generated APIs can save you a lot of time. But they also eliminate a whole category of bugs thanks to type safety. When your API eventually changes, TypeScript promptly lets you know what parts of the code need updating.

Coming up in part 2

In the next part, we’ll see how to integrate a generator into your existing project and make the best use of the CLI options.

I’ll also show you how to create reusable React hooks to query and cache the data. And if you like it, I might even teach you how to mock the server, so that you can start creating and testing your pages even before the service is implemented 😮

So when you’re ready, head this way:

--

--