Trying out NestJS part 4: Generate Typescript clients from OpenAPI documents
Tags: nestjs, react, docker, openapi
Published: 2021-03-28 13:55:27

*This article was originally published on my [personal blog](https://arnaudcortisse.com/blog).*

## Introduction

In my [last blog post](https://dev.to/arnaudcortisse/trying-out-nestjs-part-3-creating-an-openapi-document-3800), 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](https://openapi-generator.tech/docs/generators/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](https://github.com/OpenAPITools/openapi-generator-cli#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](https://github.com/OpenAPITools/openapi-generator/blob/master/docs/generators/typescript-axios.md)).

#### 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.