Trying out NestJS part 3: Creating an OpenAPI document

Introduction

In my last blog post, I showed you how easy it is to set up REST API routes using NestJS.

In this blog post, I'd like to try out the OpenAPI module NestJS provides. I'm doing it at this stage because I want to be able to test my routes easily without resorting to any external tools (like Postman).

Getting Started

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

The official documentation is here: OpenAPI.

  • Stop the running Docker containers, if any.
  • Go in packages/nestjs and run npm install --save @nestjs/swagger swagger-ui-express.
  • Go in the root directory and type in docker-compose up --build.

Now you're good to go with the OpenAPI module.

Creating the OpenAPI document

The bootstrap function (in main.ts) is where you're going to instantiate the OpenAPI module.

import { NestFactory } from '@nestjs/core'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); const config = new DocumentBuilder() .setTitle('Tasks') .setDescription('The tasks API description') .setVersion('1.0') .addTag('tasks') .build(); const document = SwaggerModule.createDocument(app, config); SwaggerModule.setup('api', app, document); await app.listen(process.env.NEST_PORT); } bootstrap();

The code speaks for itself: we're essentially setting up the module that's going to deal with the OpenAPI documentation. SwaggerModule.setup('api', app, document); makes the documentation available on the route /api.

Testing Current Setup

Once main.ts has been modified, just go over http://localhost:3001/api/ in the browser. You should have the following:

openapi_1

As you can see, the OpenAPI module in NestJS is pretty cool: it automatically picks up the existing routes and DTOs and makes them available.

However, you can see that every route is under the tab default, while I would have liked the routes related to the Tasks to be under the tab Tasks. In order to fix that, modify the tasks.controller.ts file and add the @ApiTags('tasks') decorator:

... import { ApiTags } from '@nestjs/swagger'; @ApiTags('tasks') @Controller('tasks') export class TasksController { ...

and reload http://localhost:3001/api/.

Creating the Task DTO

TaskDto is going to be a class containing an exhaustive list of all the properties a Task contains.

Let's create a file task.dto.ts inside ./packages/nestjs/tasks/dto/.

import { ApiProperty } from '@nestjs/swagger'; export class TaskDto { id: number; title: string; description: string; }

Modifying CreateTaskDto

CreateTaskDto will be used to create new Tasks. Right now it's empty (both in code and in the OpenAPI document). Let's change that:

import { PickType } from '@nestjs/swagger'; import { TaskDto } from './task.dto'; export class CreateTaskDto extends PickType(TaskDto, [ 'description', 'title', ] as const) {}

You can see that I used the utility class PickType. That way, we can keep the code as DRY as possible.

When creating a task, we want to provide a title and a description, while the ID should somehow be generated internally (we don't have any DB yet).

When I refresh http://localhost:3001/api/, I can't see any of my changes reflected in the OpenAPI document:

create-task-dto

It's because I didn't explicitly tell the OpenAPI module that I wanted the new properties to be exposed. To do so, just add the decorator @ApiProperty:

export class TaskDto { @ApiProperty() id: number; @ApiProperty() title: string; @ApiProperty() description: string; }

Now you can see those properties if you go over http://localhost:3001/api/:

create-task-dto-1

But what's cool about it is that I can now play with the OpenAPI UI more easily.

Say that you want to test the route POST /tasks. Just use the OpenAPI UI (by clicking on Try it out):

post-tasks

The request body will be prefilled with some default values and you'll be able to visualize what is your API replying very easily:

post-tasks-response

Modifying UpdateTaskDto

UpdateTaskDto will be used to update our tasks. It currently looks like this:

import { PartialType } from '@nestjs/mapped-types'; import { CreateTaskDto } from './create-task.dto'; export class UpdateTaskDto extends PartialType(CreateTaskDto) {}

We want to be able to update both the title and the description, so we don't need the utility class PartialType:

import { CreateTaskDto } from './create-task.dto'; export class UpdateTaskDto extends CreateTaskDto {}

That change is now reflected in the OpenAPI endpoint:

update-task-dto

put-tasks

Add more information about the input parameters

At present the only pieces of information we have about the title and the description is that there are strings. We could also decide to provide further information, such as the minimum and the maximum length of those strings:

export class TaskDto { @ApiProperty({ description: 'The ID of the task', default: 'This is a fake ID', }) id: number; @ApiProperty({ description: 'The title of the task', minLength: 3, maxLength: 30, default: 'This is a fake title', }) title: string; @ApiProperty({ description: 'The description of the task', minLength: 0, maxLength: 200, default: 'This is a fake description', }) description: string; }

which translates like this in the OpenAPI endpoint:

api-property-decorator

Be aware that those decorators don't perform any data validation operations. By the way, NestJS recommends the use of the class-validator package for data validation (which I'll try out later).

Add further information about the responses

Currently our API don't expose what any of its responses look like. The default is status code 200 and doesn't specify the shape of the data. Let's improve that.

In tasks.controller.ts, add these lines:

... @Get(':id') @ApiOkResponse({ description: 'The task has been successfully found.', type: TaskDto, }) findOne(@Param('id') id: string) { return this.tasksService.findOne(+id); } ...

The @ApiOkResponse decorator allows us to specify what our API responds code-wise (200 in this case, but we could have used the @ApiResponse decorator and specified the response code ourselves) as well as the shape of the data returned. It yields the following:

findone-response

You can also specify what the response looks like when you can't find the requested task:

... @ApiNotFoundResponse({ description: "Couldn't find the task", }) ...

Final Words

As you can see, getting started with OpenAPI inside NestJS is very easy. I would strongly encourage people to document their APIs using such tools: It's directly integrated inside the source code and it's very easy to maintain. The benefits of documenting your APIs are multiples. Among other things, that makes it very easy to understand and test them.

Arnaud Cortisse © 2024