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 fornestjs
to be running in order for theopenapi_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.