Trying out NestJS part 4: Generate typescript clients from OpenAPI documents

Introduction

In my last blog post, we saw how easy it is to get started with OpenAPI using NestJS.

In this blog post, I'd like to show you how you can leverage the generated OpenAPI document in order to generate a typescript client that's going to be used in the React App.

Why would I do that? I like to have statically-typed endpoints, rather than having to do the typing myself. Besides, the fact that it's automatically generated means that we can automate the generation in a CI and make sure everything is OK at compile-time.

Getting Started

The source code for this part of the project is available here: https://github.com/arnaud-cortisse/trying-out-nestjs-part-4.

OpenAPI Generator

There are plenty of tools that we can use in order to generate OpenAPI clients.

The one I'm going to use is the following: typescript-axios.

The OpenAPI document

In the last blog post I only told you about http://localhost:3001/api/, which hosts the Swagger UI.

But there is another key endpoint: http://localhost:3001/api-json. This endpoint hosts the generated OpenAPI document that we'll refer to in order to generate the client.

Setting up the environment for the OpenAPI generator

The OpenAPI generator tool requires that we install several dependencies on our machine, but I don't like to bloat my machine with project-specific dependencies.

Let's try to make use of Docker again!

Preparing the files

In the root folder, execute the following:

  • mkdir -p tools/openapi-generator
  • cd tools/openapi-generator
  • touch Dockerfile
  • touch openapitools.json
  • touch generate.sh
  • touch .gitignore

tools/openapi-generator/Dockerfile

This docker image is going to be used for generating the OpenAPI document by reaching out to NestJS's /api-json endpoint.

FROM timbru31/java-node:jdk-14 RUN npm install @openapitools/openapi-generator-cli -g RUN mkdir /local WORKDIR /local COPY . . CMD ["sh", "generate.sh"]
  • We make use of a docker image with preinstalled JDK (because openapi-generator-cli needs it).
  • We install the openapi-generator-cli.
  • We create a folder /local and copy everything that's in /tools/openapi-generator into it.
  • When starting the image we launch the script generate.sh (we still need to fill it).

tools/openapi-generator/openapitools.json

The OpenAPI generator configuration file. See Configuration for more info.

{ "$schema": "node_modules/@openapitools/openapi-generator-cli/config.schema.json", "spaces": 2, "generator-cli": { "version": "5.0.0" } }

tools/openapi-generator/generate.sh

The script that's executed when starting the newly defined Dockerfile.

openapi-generator-cli generate \ -i http://nestjs:3001/api-json \ --generator-name typescript-axios \ -o /local/out \ --additional-properties=useSingleRequestParameter=true
  • -i param indicates where the OpenAPI document is located. Here I decided to use http://nestjs:3001/api-json instead of http://localhost:3001/api-json (both work, but I prefer the former). You won't be able to access http://nestjs:3001/api-json in your browser, since it's not a name you are able to resolve on your machine (but that is resolvable within docker-compose, since both images are going to run in the same network).
  • --generator-name to indicate that generator we wanna use.
  • -o to indicate where we want to ouput the generated files.
  • --additional-properties is used to provide additional parameters to the generator (see this page).

tools/openapi-generator/.gitignore

We don't want to version the file that are output by generator in this folder (but we will version the generated files in the React App).

.build

Modifying docker-compose.yml

Let's make it possible to start openapi_generator from the existing docker-compose file.

openapi_generator: build: context: ./tools/openapi-generator dockerfile: Dockerfile depends_on: - nestjs volumes: - ./tools/openapi-generator/.build:/local/out
  • We make the service depend on nestjs. That way, nestjs is going to be started if it hadn't been already before. Indeed, it is mandatory for nestjs to be running in order for the openapi_generator to be able to generate the client API.
  • We mount the folder ./tools/openapi-generator/.build inside the service, where the client is going to be generated (we configured that path ourselves just above). That way, we get access to the generated files on the host machine.

Modifying the root package.json

In the root package.json, add the following script:

"scripts": { ... "generate-api-client": "docker-compose up --build openapi_generator" ... }

Trying out the OpenAPI Generator

In the root folder, type the following:

  • npm run generate-api-client.

If everything went fine, you should have files in this folder: tools/openapi-generator/.build.

If you don't have any files, it might be because the nestjs service wasn't ready yet when the generator tried to reach it. Just try to relaunch npm run generate-api-client and everything should be OK.

Delivering the client to the React App.

In the root folder, execute the following:

  • mkdir scripts
  • touch scripts/update-api.sh

update-api.sh

#!/bin/bash cd "$(dirname "$0")" SOURCE_FOLDER="../tools/openapi-generator/.build" DEST_FOLDER="../packages/react-app/src/api/generated" rm -rf $DEST_FOLDER mkdir -p $DEST_FOLDER cp $SOURCE_FOLDER/**.ts $DEST_FOLDER

With this script, we essentially are delivering the files generated automatically by the service openapi_generator to the React App.

Modifying the root package.json

In the root package.json, add the following scripts:

"scripts": { ... "update-api-client": "sh ./scripts/update-api.sh", "generate-and-update-api-client": "npm run generate-api-client && npm run update-api-client" ... }

Trying out the delivery mechanism

In the root folder, type the following:

  • npm run generate-and-update-api-client.

If everything went well, you should have files in packages/react-app/src/api/generated.

Make use of the client in the React App

Installing new dependencies

In the packages/react-app/src directory, execute the following:

  • npm install axios react-query

Deleting some files

  • cd packages/react-app/src
  • rm App.css App.test.tsx App.tsx

Creating new files

  • cd packages/react-app/src
  • mkdir axios
  • mkdir api (but it should already exist)
  • mkdir components
  • touch axios/axios-client.ts
  • touch api/api.ts
  • touch components/App.tsx
  • touch components/Example.tsx

packages/react-app/src/axios/axios-client.ts

Used to configure an axios instance so that it is preconfigured to reach to NestJS.

import axios, { AxiosRequestConfig } from "axios"; export const axiosBaseUrl = `${process.env.REACT_APP_BACKEND_SCHEMA}://${process.env.REACT_APP_BACKEND_HOSTNAME}:${process.env.REACT_APP_BACKEND_PORT}`; export const axiosConfig: AxiosRequestConfig = { baseURL: axiosBaseUrl, }; const axiosBackendClient = axios.create(axiosConfig); export default axiosBackendClient;

packages/react-app/src/api/api.ts

Configuration of an instance of TasksApi (a class automatically generated by the generator) that we'll use to communicate with our backend.

import axiosBackendClient, { axiosBaseUrl } from "../axios/axios-client"; import { TasksApi } from "./generated"; export const tasksApi = new TasksApi( { basePath: axiosBaseUrl, isJsonMime: () => false, }, undefined, axiosBackendClient );

packages/react-app/src/components/App.tsx

import React from "react"; import { QueryClient, QueryClientProvider } from "react-query"; import Example from "./Example"; const queryClient = new QueryClient(); export default function App() { return ( <QueryClientProvider client={queryClient}> <Example /> </QueryClientProvider> ); }
  • We configure the react-query provider.
  • We render the Example component (yet to be defined).

packages/react-app/src/components/Example.tsx

import { useQuery } from "react-query"; import { tasksApi } from "../api/api"; export default function Example() { const id = "fake id"; const { isLoading, error, data } = useQuery(`tasks_find_one_${id}`, () => tasksApi.tasksControllerFindOne({ id, }) ); if (isLoading) return <div>Loading...</div>; if (error as Error) return <div>An error has occurred</div>; return <div>{data?.data.title}</div>; }

Have a look at the query. This is where the magic happens: we make use of the automatically-generated client and, as a result, have all the benefits of static types.

Modifying existing files

packages/react-app/src/index.tsx

I just removed some useless lines (in the context of this blog) and imported the App component from the appropriate path.

import React from "react"; import ReactDOM from "react-dom"; import "./index.css"; import App from "./components/App"; ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode>, document.getElementById("root") );

Trying out the client

In the root folder, perform the following:

  • docker-compose up --build (might take a while since there are new dependencies in the React App to be installed).

Go over http://localhost:3000/ in your browser.

You should have the following message at some point: An error has occurred.

Open your developer tools: you should see a CORS error. We can fix that by updating the Nest app.

Enable CORS

In packages/nestjs/src/main.ts, add the following

... app.enableCors(); ...

Mind you, you should definitely configure the CORS rules appropriately in a production environment.

Testing everything out

Now, if you go on http://localhost:3000/ in your browser, you should see the message fake title.

It means we are indeed able to communicate with our API using an automatically-generated client.

Final Words

Setting everything up wasn't straightforward. Nevertheless, we now have a nice way of communicating with our API: we have a typed client that's going to greatly improve the development experience inside React. What's more, it basically costs nothing to regenerate that client so that it matches the latest API. Lastly, we are now able to detect any desynchronization between the React App and the NestJS app at compile time.

Arnaud Cortisse © 2024