Skip to content

Introduction to RESTful APIs with NestJS

This article was written over 18 months ago and may contain information that is out of date. Some content may be relevant but please refer to the relevant official documentation or available resources for the latest information.

Welcome to an introduction to RESTful APIs with NestJS. Understanding JavaScript and TypeScript will make it easier to follow the directions in this article, but you don't necessarily need to be proficient.

NestJS is one of the most rapidly growing frameworks for building Node.js server-side applications. Companies such as Roche, Adidas, and Autodesk, all trust NestJS when it comes to building efficient and scalable server-side applications.

NestJS is based heavily on Angular, and uses Angular-like modules, services, controllers, pipes, and decorators. This allows NestJS to help developers create scalable, testable, loosely coupled, and easily maintainable applications. NestJS was built with TypeScript, but it can also support pure JavaScript development.

NestJS offers a level of abstraction above two very popular Node.js frameworks, either Express.js or Fastify. This means that all of the great middleware that are available for Express.js and Fastify can also be used with NestJS.

The best way to get familiar with NestJS is to build a basic RESTful API with CRUD (Create, Read, Update, and Delete) functionality. This is exactly what we'll be doing together in this article. We'll be building a simple RESTful API for a Blog, with endpoints to handle CRUD operations on blog posts.

Getting Started

Code editor

Using Visual Studio Code as a code editor can help speed up NestJS development because of its smart IntelliSense and great TypeScript support.

In Visual Studio Code, make sure that you have the following user setting by going to File / Preferences / Settings, and searching for the user setting named typescript.preferences.importModuleSpecifier. Make sure to set it to relative as seen below.

"typescript.preferences.importModuleSpecifier": "relative"

This will allow Visual Studio Code to use relative paths rather than absolute paths when auto-importing. Using absolute path imports in our application can lead to problems if and when our code ends up in a different directory.

Insomnia

Insomnia is a useful API testing tool that we will use to test the NestJS API that we will be building.

The NestJS CLI

To get started with NestJS, let's install the Nest CLI. The Nest CLI is a command-line interface tool that makes it easy to develop, and maintain NestJS applications. It allows us to run our application in development mode, and to build and bundle it for a production-ready release.

npm i -g @nestjs/cli

Creating a new NestJS project

With the Nest CLI now installed, we can use it to create a new project.

nest new rest-api

This command will create a new project directory called rest-api. It will create a base structure for our project, and add in the following core Nest files:

  • app.controller.ts: A controller with a single route.
  • app.controller.spec.ts: The controller's unit tests.
  • app.module.ts: The root module of our application.
  • app.service.ts: A service for the AppModule's business logic.
  • main.ts: The entry file of our application.

The initial project structure created by the Nest CLI encourages us to follow the common convention of keeping each module in its own directory.

Testing the sample endpoint

The installation of NestJS comes with a sample API endpoint that we can test by making a request to it. If we open app.controller.ts, we can see that there is a GET endpoint that was created for us with the @Get() decorator. It returns a 'Hello World!' string.

@Get()
getHello(): string {
  return this.appService.getHello();
}

Let's run npm run start:dev from our project folder. This will run our NestJS app in watch mode, which provides live-reload support when application files are changed.

Once NestJS is running, let's open http://localhost:3000 in our web browser. We should see a blank page with the Hello World! greeting.

We can also use API testing tools such as Insomnia to make a GET request to http://localhost:3000. We should get the same Hello World! greeting as our result.

Let's remove this endpoint since it was only added by the Nest CLI for demo purposes. Go ahead and delete app.controller.ts, app.service.ts, and app.controller.spec.ts. Also, delete all references to AppController and AppService in app.module.ts.

Creating a feature module

The architectural design of NestJS encourages feature modules. This feature-based design groups the functionality of a single feature in one folder, registered in one module. This design simplifies the codebase and makes code-splitting very easy.

Module

We create modules in NestJS by decorating a class with the @Module decorator. Modules are needed to register controllers, services, and any other imported sub-modules. Imported sub-modules can have their own controllers, and services, registered.

Let's use the Nest CLI to create the module for our blog posts.

nest generate module posts

This gives us an empty PostsModule class in the posts.module.ts file.

Interface

We will use a TypeScript interface to define the structure of our JSON object that will represent a blog post.

An interface is a virtual or abstract structure that only exists in TypeScript. Interfaces are used only for type-checking purposes by the TypeScript compiler. TypeScript interfaces do not produce any JavaScript code during the transpilation of TypeScript to JavaScript.

Let's use the Nest CLI to create our interface.

cd src/posts 
nest generate interface posts

These commands allow us to create a posts.interface.ts file in the feature-based folder for our blog posts, which is /src/posts.

The TypeScript interface keyword is used to define our interface. Let's make sure to prefix it with export to allow this interface to be used throughout our application

export interface PostModel {
  id?: number;
  date: Date;
  title: string;
  body: string;
  category: string;
}

From the command-line prompt, let's reset our current working directory back to the root folder of our project by using the following command.

cd ../..

Service

Services are classes that handle business logic. The PostsService that we will be creating will handle the business logic needed to manage our blog posts.

Let's use the Nest CLI to create the service for our blog posts.

nest generate service posts

This will give us an empty PostsService class in the posts.service.ts file.

The @Injectable() decorator marks the PostsService class as a provider that we can register in the providers array of our PostsModule, and then inject into our controller class. More on this later.

Controller

Controllers are classes that handle incoming requests and return responses to the client. A controller can have more than one route or endpoint. Each route or endpoint can implement its own set of actions. The NestJS routing mechanism handles the routing of requests to the right controller.

Let's use the Nest CLI to create the controller for our blog posts.

nest generate controller posts

This will give us an empty PostsController class in the posts.controller.ts file.

Let's inject our PostsService into the constructor of the PostsController class.

import { Controller } from '@nestjs/common';
import { PostsService } from './posts.service';

@Controller('posts')
export class PostsController {
  constructor(private readonly postsService: PostsService) {}
}

NestJS uses dependency injection to set up a reference to PostsService from within our controller. We can now use the methods provided by PostsService by referencing this.postsService from within our controller class.

Registering our controller and service

Let's make sure that our PostsModule registers our PostsController and PostsService. The nest generate service post and nest generate controller post commands that we ran earlier have automatically registered the PostsService and PostsController classes in the PostsModule for us.

@Module({
  controllers: [PostsController],
  providers: [PostsService],
})
export class PostsModule {}

NestJS uses the term providers to refer to service classes, middleware, guards, and more.

If the PostsService class needs to be made available to other modules within our application, we can export it from PostsModule by using the exports array. This way, any module importing the PostsModule will be able to use the PostsService.

@Module({
  controllers: [PostsController],
  providers: [PostsService],
  exports: [PostsService],
})
export class PostsModule {}  

Importing PostsModule into AppModule

We now have one cohesive and well-organized module for all the functionality related to our blog posts. However, all the functionality that will be provided by our PostsModule is not available to our application unless the AppModule imports it.

If we revisit the AppModule, we can see that the PostsModule has been automatically added to the imports array by the nest generate module post command that we ran earlier.

@Module({
  imports: [PostsModule],
  controllers: [],
  providers: [],
})
export class AppModule {}

Adding Service and Controller logic

Our PostsService and PostsController have no functionality implemented at the moment. Let's now implement our CRUD endpoints and their corresponding logic while adhering to RESTful standards.

NestJS makes it easy to use MongoDB (using the @nestjs/mongoose package), or PostgreSQL (using Prisma or TypeORM) to persist and manage our application's data. However, for the sake of simplicity, let's create a local array in our service to mock a database.

import { Injectable } from '@nestjs/common';
import { PostModel } from './posts.interface';

@Injectable()
export class PostsService {
  private posts: Array<PostModel> = [];
}

A service, or provider, as NestJS refers to them, can have its scope configured. By default, a service has a singleton scope. This means that a single instance of a service is shared across our entire application. The initialization of our service will occur only once during the startup of our application. The default singleton scope of services means that any class that injects the PostsService will share the same posts array data in memory.

Get all posts

Let's add a method to our PostsService that will return all of our blog posts.

public findAll(): Array<PostModel> {
  return this.posts;
}

Let's add a method to our PostsController that will make the logic of the service's findAll() method available to client requests.

@Get()
public findAll(): Array<PostModel> {
  return this.postsService.findAll();
}

The @Get decorator is used to create a GET /posts endpoint. The /posts path of the request comes from the @Controller('posts') decorator that was used in order to define our controller.

Get one post

Let's add a method to our PostsService that will return a specific blog post that a client may be looking for. If the id of the requested post is not found in our list of posts, let's return an appropriate 404 NOT FOUND HTTP error.

public findOne(id: number): PostModel {
  const post: PostModel = this.posts.find(post => post.id === id);

  if (!post) {
    throw new NotFoundException('Post not found.');
  }

  return post;
}

Let's add a method to our PostsController that will make the logic of the service's findOne() method available to client requests.

@Get(':id')
public findOne(@Param('id', ParseIntPipe) id: number): PostModel {
  return this.postsService.findOne(id);
}

The @Get decorator is used with a parameter as @Get(':id') here to create a GET /post/[id] endpoint, where [id] is an identification number for a blog post.

@Param, from the @nestjs/common package, is a decorator that makes route parameters available to us as properties in our method.

@Param values are always of type string. Since we defined id to be of type number in TypeScript, we need to do a string to number conversion. NestJS provides a number of pipes that allow us to transform request parameters. Let's use the NestJS ParseIntPipe to convert the id to a number.

Create a post

Let's add a method to our PostsService that will create a new blog post, assign the next sequential id to it, and return it. If the title is already being used by an existing blog post, we will throw a 422 UNPROCESSABLE ENTITY HTTP error.

public create(post: PostModel): PostModel {
  // if the title is already in use by another post
  const titleExists: boolean = this.posts.some(
    (item) => item.title === post.title,
  );
  if (titleExists) {
    throw new UnprocessableEntityException('Post title already exists.');
  }

  // find the next id for a new blog post
  const maxId: number = Math.max(...this.posts.map((post) => post.id), 0);
  const id: number = maxId + 1;

  const blogPost: PostModel = {
    ...post,
    id,
  };

  this.posts.push(blogPost);

  return blogPost;
}

Let's add a method to our PostsController that will make the logic of the service's create method available to client requests.

@Post()
public create(@Body() post: PostModel): PostModel {
  return this.postsService.create(post);
}

The @Post decorator is used to create a POST /post endpoint.

When we use the NestJS decorators for the POST, PUT, and PATCH HTTP verbs, the HTTP body is used to transfer data to the API, typically in JSON format.

We can use the @Body decorator to parse the HTTP body. When this decorator is used, NestJS will run JSON.parse() on the HTTP body and provide us with a JSON object for our controller method. Within this decorator, we declare post to be of type Post because this is the data structure that we are expecting the client to provide for this request.

Delete a post

Let's add a method to our PostsService that will delete a blog post from our in-memory list of posts using the JavaScript splice() function. A 404 NOT FOUND HTTP error will be returned if the id of the requested post is not found in our list of posts.

public delete(id: number): void {
  const index: number = this.posts.findIndex(post => post.id === id);

  // -1 is returned when no findIndex() match is found
  if (index === -1) {
    throw new NotFoundException('Post not found.');      
  }

  this.posts.splice(index, 1);
}

Let's add a method to our PostsController that will make the logic of the service's delete method available to client requests.

@Delete(':id')
public delete(@Param('id', ParseIntPipe) id: number): void {  
  this.postsService.delete(id);
}

Update a post

Let's add a method to our PostsService that will find a blog post by id, update it with newly submitted data, and return the updated post. A 404 NOT FOUND HTTP error will be returned if the id of the requested post is not found in our list of posts. A 422 UNPROCESSABLE ENTITY HTTP error will be returned if the title is being used for another blog post.

public update(id: number, post: PostModel): PostModel {
  this.logger.log(`Updating post with id: ${id}`);

  const index: number = this.posts.findIndex((post) => post.id === id);

  // -1 is returned when no findIndex() match is found
  if (index === -1) {
    throw new NotFoundException('Post not found.');
  }

  // if the title is already in use by another post
  const titleExists: boolean = this.posts.some(
    (item) => item.title === post.title && item.id !== id,
  );
  if (titleExists) {
    throw new UnprocessableEntityException('Post title already exists.');
  }

  const blogPost: PostModel = {
    ...post,
    id,
  };

  this.posts[index] = blogPost;

  return blogPost;
}

Let's add a method to our PostsController that will make the logic of the service's update method available to client requests.

@Put(':id')
public update(@Param('id', ParseIntPipe) id: number, @Body() post: PostModel): PostModel {
  return this.postsService.update(id, post);
}

We use the @Put decorator to make use of the HTTP PUT request method. PUT can be used to both create and update the state of a resource on the server. If we know that a resource already exists, PUT will replace the state of that resource on the server.

Testing our feature module

Let's start our development server using npm run start:dev. Then, let's open the Insomnia application to test the API endpoints that we created for the PostsModule.

Get all posts

Let's make a GET request to http://localhost:3000/posts. The result should be a 200 OK success code with an empty array.

Create a post

Let's make a POST request to http://localhost:3000/posts using the following as our JSON body.

{
  "date": "2021-08-16",
  "title": "Intro to NestJS",
  "body": "This blog post is about NestJS",
  "category": "NestJS"
}

We should get a successful 201 Created response code, along with a JSON representation of the post that was created, including the id field that has been automatically generated.

Get a post

Let's make a GET request to http://localhost:3000/posts/1. The result should be a successful 200 OK response code, along with the data for the post with an id of 1.

Update a post

Let's make a PUT request to http://localhost:3000/posts/1 using the following as our JSON body.

{
  "date": "2021-08-16",
  "title": "Intro to TypeScript",
  "body": "An intro to TypeScript",
  "category": "TypeScript"
}

The result should be a successful 200 OK response code, along with a JSON representation of the post that was updated.

Delete a post

Let's make a DELETE request to http://localhost:3000/posts/1. The result should be a successful 200 OK response code with no JSON object returned.

Logging

NestJS makes it easy to add logging to our application. Rather than using console.log() statements, we should use the Logger class provided by NestJS. This will provide us with nicely formatted log messages in the Terminal.

Let's add logging to our API. The first step is to define the logger in our service class.

import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import { PostModel } from './posts.interface';

@Injectable()
export class PostsService {
  private posts: Array<PostModel> = [];
  private readonly logger = new Logger(PostsService.name);

  // ...
}

With the logger now defined, we can add log statements in our service class. Here is an example of a log statement that we can add as the first line of the findAll() method in the PostsService class.

this.logger.log('Returning all posts');

Such statements provide us with a convenient log message in the Terminal every time a service method is called during a client's request to our API.

When a GET /posts request is sent, we should see the following message in the Terminal.

[PostsService] Returning all posts.

Swagger

NestJS makes it easy to add the OpenAPI specification to our API using the NestJS Swagger package. The OpenAPI specification is used to describe RESTful APIs for documentation and reference purposes.

Swagger setup

Let's install Swagger for NestJS.

npm install --save @nestjs/swagger swagger-ui-express

Swagger configuration

Let's update the main.ts file that bootstraps our NestJS application by adding Swagger configuration to it.

import { NestFactory } from '@nestjs/core';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  const config = new DocumentBuilder()
    .setTitle('Blog API')
    .setDescription('Blog API')
    .setVersion('1.0')
    .build();

  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('api', app, document);

  await app.listen(3000);
}
bootstrap();

With our NestJS app running, we can now go to http://localhost:3000/api to view the Swagger documentation for our API. Notice that default is displayed above the routes for our posts.

Let's change that by adding @ApiTags('posts') right below @Controller('posts') in our PostsController class. This will replace default with posts to indicate that this set of endpoints belongs to the posts feature model.

@Controller('posts')
@ApiTags('posts')

ApiProperty

Let's make the properties of our PostModel interface visible to Swagger. We do so by annotating fields with the ApiProperty() decorator, or the ApiPropertyOptional() decorator for optional fields.

To use these decorators, we must first change our interface to a class in the posts.interface.ts file.

export class PostModel {
  @ApiPropertyOptional({ type: Number })
  id?: number;
  @ApiProperty({ type: String, format: 'date-time' })
  date: Date;
  @ApiProperty({ type: String })
  title: string;
  @ApiProperty({ type: String })
  body: string;
  @ApiProperty({ type: String })
  category: string;
}

Within the ApiProperty() decorator applied to each field, we describe the field type. The id field is optional since we don't know of a new blog post's id when creating it. A date-time string format is used for the date field.

These changes allow the PostModel structure to be documented within Swagger. With our NestJS app running, we can now go to http://localhost:3000/api to view the documentation of the PostModel.

ApiResponse

Let's use the @ApiResponse() decorator for Swagger to document all the possible responses from our API's endpoints. This will be helpful for informing users of our API what responses they can expect to receive by calling a given endpoint. We will be making these changes in the PostsController class.

For the findAll method, let's use the @ApiOkResponse() decorator to document a 200 OK success response.

@Get()
@ApiOkResponse({ description: 'Posts retrieved successfully.'})
public findAll(): Array<PostModel> {
  return this.postsService.findAll();
}

For the findOne method, let's use the @ApiOkResponse() decorator to document a 200 OK response when the post is found. Let's use the @ApiNotFoundResponse() decorator to document a 404 NOT FOUND HTTP error when a post is not found.

@Get(':id')
@ApiOkResponse({ description: 'Post retrieved successfully.'})
@ApiNotFoundResponse({ description: 'Post not found.' })
public findOne(@Param('id', ParseIntPipe) id: number): PostModel {
  return this.postsService.findOne(id);
}

For the create method, let's use the @ApiCreatedResponse() decorator to document a 201 CREATED response when a post is created. Let's use the @ApiUnprocessableEntityResponse() decorator to document a 422 UNPROCESSABLE ENTITY HTTP error when a duplicate post title is found.

@Post()
@ApiCreatedResponse({ description: 'Post created successfully.' })
@ApiUnprocessableEntityResponse({ description: 'Post title already exists.' })
public create(@Body() post: PostModel): void {
  return this.postsService.create(post);
}

For the delete method, let's use the @ApiOkResponse() decorator to document a 200 OK response when a post is deleted. Let's use the @ApiNotFoundResponse() decorator to document a 404 NOT FOUND HTTP error when the post to be deleted is not found.

@Delete(':id')
@ApiOkResponse({ description: 'Post deleted successfully.'})
@ApiNotFoundResponse({ description: 'Post not found.' })
public delete(@Param('id', ParseIntPipe) id: number): void {
  return this.postsService.delete(id);
}

For the update method, let's use the @ApiOkResponse() decorator to document a 200 OK response when a post is updated. Let's use the @ApiNotFoundResponse() decorator to document a 404 NOT FOUND HTTP error when the post to be updated is not found. Let's use the @ApiUnprocessableEntityResponse() decorator to document a 422 UNPROCESSABLE ENTITY HTTP error when a duplicate post title is found.

@Put(':id')
@ApiOkResponse({ description: 'Post updated successfully.'})
@ApiNotFoundResponse({ description: 'Post not found.' })
@ApiUnprocessableEntityResponse({ description: 'Post title already exists.' })
public update(@Param('id', ParseIntPipe) id: number, @Body() post: PostModel): void {
  return this.postsService.update(id, post);
}

After saving these changes, all response codes and their related descriptions should now be listed for each endpoint on the Swagger web page at http://localhost:3000/api.

We could also use Swagger instead of Insomnia to test our API. By clicking on the "Try it out" button that appears under each endpoint on the Swagger web page, we can see if they are working as expected.

Exception filter

Exception filters give us full control over the exceptions layer of NestJS. We can use an exception filter to add custom fields to a HTTP exception response body or to print out logs of every HTTP exception that occurs to the Terminal.

Let's create a new filters folder in the /src folder, as well as a new file called http-exception.filter.ts within it. Let's add the following class in this file.

import { ExceptionFilter, Catch, ArgumentsHost, HttpException, Logger } from '@nestjs/common';
import { Request, Response } from 'express';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  private readonly logger = new Logger(HttpExceptionFilter.name);

  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    const statusCode = exception.getStatus();
    const message = exception.message || null;

    const body = {
      statusCode,
      message,      
      timestamp: new Date().toISOString(),
      endpoint: request.url,
    };

    this.logger.warn(`${statusCode} ${message}`);

    response
      .status(statusCode)
      .json(body);
  }
}

This class makes use of the NestJS logger to print a warning message to the Terminal whenever a HTTP exception occurs. It also returns two custom fields within the response body whenever a HTTP exception occurs. The timestamp field reports when the exception occurred, and the endpoint field reports which route triggered the exception.

In order to apply this filter, we need to decorate the PostsController with @UseFilters(new HttpExceptionFilter()).

@Controller('posts')
@UseFilters(new HttpExceptionFilter())
export class PostsController {
  constructor(private readonly postsService: PostsService) {}
}

After saving these changes, NestJS will reload our application for us. If we use Insomnia to send a PUT /posts/1 request to our API, it should trigger a 404 NOT FOUND HTTP error, since no blog posts exist for updating in our application when it starts. The HTTP exception response body that is returned to Insomnia should now contain the timestamp and endpoint fields.

{
  "statusCode": 404,
  "message": "Post not found.",
  "timestamp": "2021-08-23T21:05:29.497Z",
  "endpoint": "/posts/1"
}

We should also see the following line printed out to the Terminal.

WARN [HttpExceptionFilter] 404 Post not found.

Summary

In this article, we saw how NestJS makes back-end API development fast, simple, and effective. The NestJS application structure has helped us build a very well-organized project.

We covered a lot, so let's recap what we learned:

  • How to use the NestJS CLI.
  • How to build feature modules in NestJS.
  • How to use services and controllers.
  • How to test an API with Insomnia.
  • How to add logging to NestJS apps.
  • How to use Swagger for documenting and previewing NestJS APIs.
  • How to get full control of HTTP exceptions in NestJS.

Happy developing with NestJS!

Code

Visit the following GitHub repository in order to access the code featured in this article.

This Dot is a consultancy dedicated to guiding companies through their modernization and digital transformation journeys. Specializing in replatforming, modernizing, and launching new initiatives, we stand out by taking true ownership of your engineering projects.

We love helping teams with projects that have missed their deadlines or helping keep your strategic digital initiatives on course. Check out our case studies and our clients that trust us with their engineering.

Let's innovate together!

We're ready to be your trusted technical partners in your digital innovation journey.

Whether it's modernization or custom software solutions, our team of experts can guide you through best practices and how to build scalable, performant software that lasts.

Prefer email? hi@thisdot.co