Generating a strongly typed REST API for Nest.js
17 mars 2023
Nos experts partagent leurs expériences sur le blog. Contactez-nous pour discuter de vos projets !
Hi there!
Today we’ll talk about a tool we made here at LoneStone, the Nest.js SDK generator.
A common problem while developing REST APIs and web clients is that the client must reflect API changes or the API must be backward compatible. For small teams this can create a lot of regressions during development.
Some solutions involve monorepos and shared DTOs, however it’s still the client’s responsability to list all the API endpoints, and this usually involves magic strings.
Here is a concrete example: we have a module named ArticleModule, exposing an ArticleController, which looks like this:
import { Get, Param } from '@nestjs/common'
import { ArticleService } from './article.service'
import { ArticleCreateDTO } from './dtos/article-create.dto'
@Controller('article')
export class ArticleController {
constructor(private readonly articleService: ArticleService) {}
@Get()
getAll() {
return this.articleService.getAll()
}
@Get(':id')
getOne(@Param('id') id: string) {
return this.articleService.findOne(id)
}
@Post()
create(@Body() dto: ArticleCreateDTO) {
return this.articleService.create(dto)
}
}
A very simple and basic controller. To call these endpoints we have multiple options, depending on the framework and the level of safety we want to get. The most basic option is to use the native fetch function. Or we can use an Axios instance.
These solutions are usually far from being perfect, as you have to write your API’s endpoint URI by hand (even the full URL with fetch), provide the data to send and get the response that will be casted into the expected response type.
The problem with this is that there are multiple situations were you will have to remember to update your code, or else your application will crash or have an unexpected behaviour, notably:
If a method’s endpoints change (magic string)
If a method’s expected parameters change
If a method’s expected body content change
If a method’s response type change
But what if the API automatically exported a strongly typed, complete SDK, like GraphQL tools do? That would avoid to maintain a separate set of types for the frontend to communicate with the API. So we have been working on such a tool at Lonestone: the Nest.js SDK generator
Making a SDK by hand
If we want to make a SDK, the “traditional” way, we’d probably write something like this:
// We'll have to update this each time the backend entity changes
export interface Article {
id: string
title: string
content: string
authorId: string
categoryId: string
}// We'll have to update this each time the backend DTO changes
export interface ArticleCreateDTO {
title: string
content: string
authorId?: string
categoryId?: string
}function apiCall<T>(uri: string, body?: unknown): Promise<T> {
const params = {
method: body ? 'POST' : 'GET',
headers: { 'Content-Type': 'application/json' }
}if (body) {
params.body = JSON.stringify(body)
}const response = await fetch("<https://localhost:3000/>" + uri, params)
if (!response.ok) {
throw new Error('Request failed: ' + res.statusText)
}
return response.json() as T
}export function getAll(): Promise<Article[]> {
// We'll have to update this if either the URI, the parameters of the return type change
return apiCall('article')
}export function getOne(id: string): Promise<Article> {
// We'll have to update this if either the URI, the parameters of the return type change
return apiCall('article/' + id)
}export function create(dto: ArticleCreateDTO): Promise<Article> {
// We'll have to update this if either the URI, the parameters of the return type change
return apiCall('article', dto)
}
But as you can see, this is quite verbose, requires a bit of by-hand maintanance and is overall not idea.
Generating a strongly-typed SDK conforming to your API
The generator goes through the source code of your API, checks the modules and controllers inside, and then generates a set of files which expose methods to call your API. To be concrete, with the controller we’ve seen above, instead of the large piece of code we’ve shown, we’ll write this:
That’s right, nothing! The SDK automatically generates the entities’ types for us, as well as the routes, so we have nothing to do! Now, to use the API, in the case we’d import everything from the file above and use it, with the SDK we’d do:
import { articleController } from './sdk/articleModule'
// The type of 'articles' is 'Article[]'
// The 'Article' type is part of the SDK
const articles = await articleController.getAll()
alert('List of articles:\n' + articles.map(article => '* ' + article.title).join('\n'))
// To create an article:
const created = await articleController.create({}, {
title: 'Hello world!',
content: 'Hello world and have an amazing day!'
})
// To get a specific article:
const article = await articleController.getOne({ id: created.id })
If we take a quick glance at the SDK’s generated code, we’ll see it takes three arguments: the parameters (e.g. id), the body's content, and the query (?x=y). Given that for .getAll() none of them are expected, we don't have to provide anything.
Step-by-step usage
So, let’s see how we can generate our own SDK! Let’s assume your API is stored in a server directory, and your frontend in front.
As the SDK will be used in the frontend, we’ll have to create a configuration file which tells the SDK generator how to generate the SDK for us. So, let’s create a front/sdk-generator.json file with the following content:
{
"apiInputPath": "../server",
"sdkOutput": "src/sdk",
"sdkInterfacePath": "src/sdk-interface.ts"
}
Let’s see how this works. First, we have apiInputPath, which is the path to our Nest.js API's source code. The SDK generator will look for your tsconfig.json in this directory.
Then we have sdkOutput, which is where the SDK will be generated. Here, we want to include it in our frontend's sources, so we put it here.
Finally we have sdkInterfacePath, which points to a small TypeScript file that's in charge of making the requests for us. The SDK works by making all routes use a central handler with the call's method, URI, body content, and query parameters, and the handler makes the request, adding if required any authorization header, enabling specific options, or removing/encrypting some content, and then return the parsed response.
We don’t have to create this file because by default the generator will create it for us if it doesn’t exist already, and will populate it with the default request handler which uses fetch to not depend on any other package. But you can customise this file if needed, it won't be overridden.
Now that our configuration file is ready, let’s generate the SDK:
npx nest-sdk-generator front/sdk-generator.json
This should create both the SDK and the interface. If you want to get a more detailed output, set the verbose option to true in the configuration file.
That’s it! Now we can directly use the SDK, there aren’t any additional steps. In case you update your API and need to regenerate the SDK, simply run the same command again. You can also put it in your package.json as a script. Same thing it want to rebuild the SDK automatically everytime you modify the API:
{
// ...
"scripts": {
"sdk:gen": "npx nest-sdk-generator front/sdk-generator.json",
"sdk:watch": "nodemon -e ts,json -w your/server/src/ -x \"npm run sdk:gen\""
}
// ...
}
More examples
You can find a complete demo with a small React frontend in the SDK generator’s repository.
If you want to check the list of all options and additional informations about the performances part and the limitations of the tool, check out the README!
If you want to contact us for anything related to this project, be it for using in a commercial project (did we say the generator was MIT-licensed so you can freely use it and tweak it to your needs?), don’t hesitate to get in touch!