close icon
Nest

Developing a Secure API with NestJS: Creating Endpoints

Learn how to use NestJS, a Node.js framework powered by TypeScript, to build a secure API.

Last Updated On: September 01, 2020

Are you starting from this chapter?

Clone the application repo and check out the creating-data-models-and-service branch:

git clone git@github.com:auth0-blog/wab-menu-api-nestjs.git \
nest-restaurant-api \
--branch creating-data-models-and-service

Make the project folder your current directory:

cd nest-restaurant-api

Then, install the project dependencies:

npm i

Finally, create a .env hidden file:

touch .env

Populate .env with this:

PORT=7000

Define the Endpoints

For this application, you'll create endpoints to access an items resource to perform read, write, update, and delete operations on menu items:

# get all items
GET items/
# get a single item using an id parameter
GET items/:id
# create an item
POST items/
# update an item
PUT items/
# delete an item using an id parameter
DELETE items/:id

Only the GET endpoints will be public, so let's start with those.

Create a NestJS Controller

To create an ItemsController, execute the following command:

nest generate controller items --no-spec

Once executed, this command creates an items.controller.ts file under the src/items directory. The CLI automatically registers ItemsController as a controller of ItemsModule for you.

// src/items/items.module.ts

import { Module } from '@nestjs/common';
import { ItemsService } from './items.service';
import { ItemsController } from './items.controller';

@Module({
  providers: [ItemsService],
  controllers: [ItemsController],
})
export class ItemsModule {}

Your application will only recognize controllers registered in the metadata object that the @Module decorator receives as argument.

Following the architectural conventions of other frameworks, NestJS controllers are responsible for mapping endpoints to functionality. Controllers are created using a class and a decorator as follows:

@Controller('path')
export class NameController {}

The @Controller decorator takes as argument the resource path that it will handle.

Populate src/items/items.controller.ts as follows to create all of your application endpoints:

import {
  Body,
  Controller,
  Delete,
  Get,
  Param,
  Post,
  Put,
} from '@nestjs/common';
import { ItemsService } from './items.service';
import { Items } from '../items';
import { Item } from '../item';

@Controller('items')
export class ItemsController {
  constructor(private readonly itemsService: ItemsService) {}

  @Get()
  async findAll(): Promise<Items> {
    return this.itemsService.findAll();
  }

  @Get(':id')
  async find(@Param('id') id: number): Promise<Item> {
    return this.itemsService.find(id);
  }

  @Post()
  async create(@Body('item') item: Item): Promise<void> {
    this.itemsService.create(item);
  }

  @Put()
  async update(@Body('item') item: Item): Promise<void> {
    this.itemsService.update(item);
  }

  @Delete(':id')
  async delete(@Param('id') id: number): Promise<void> {
    this.itemsService.delete(id);
  }
}

Let's explore in detail what's happening within ItemsController.

All of your endpoints share the same resource path: items/. You pass the path without the trailing forward slash to the @Controller decorator as argument.

You import the Item and Items classes to add typings to your controller handlers as needed.

To handle specific HTTP requests that use the controller's path, like GET or POST, you create class methods for each one of them. These methods are called route handlers and use HTTP request method decorators, like @Get or @Post, that match the type of request they handle:

  @Post()
  async create(@Body('item') item: Item): Promise<void> {
    this.itemsService.create(item);
  }

The function name of the route handler is not relevant to how your API is consumed by clients, but it helps you to easily identify the method for code maintenance and debugging.

Notice that the findAll handler is an async function. Why's that?

  @Get()
  async findAll(): Promise<Items> {
    return this.itemsService.findAll();
  }

As mentioned in the Asynchronicity document, data extraction is mostly asynchronous. For this reason, NestJS supports modern async functions, which must return a Promise. As such, you can return deferred values that NestJS will be able to resolve by itself.

In order to keep ItemsController lightweight, you delegate the business logic of each endpoint to methods from ItemsService. To access this service within your controller class, you inject an instance of the ItemsService class into it through its class constructor. The service instance is marked private readonly to make this instance unchangeable and only visible inside the controller class.

constructor(private readonly itemsService: ItemsService) { }

Make API Requests

With the service and controller in place, you can start the API and make a request to GET items/:

# start the development server
npm run start:dev

# make a request to GET items/
curl http://localhost:7000/items/

If the app is working as expected, you get an object containing items data as response. The curl command works on both macOS and Linux terminal apps as well as on Windows PowerShell.

Developing on Windows? Learn more about Windows PowerShell Commands for Web Developers.

When handling requests that have a payload, you need to extract the payload data from the body property of the request object that the route handler receives. With that goal in mind, you use the @Body() parameter decorator to pluck properties from the body property. @Body() receives the name of the property that you want to pluck as an argument; in this case, you want the item property:

@Post()
create(@Body('item') item: Item) {
  this.itemsService.create(item);
}

The argument passed to the @Controller() decorator defines the route path prefix, which is a path shared by all the endpoints defined within the controller. You can then create different path branches by using route parameters, such as :id. This allows you to create two distinct GET endpoints within the same controller:

GET items/
GET items/:id

You then pass the route parameter, :id, as an argument to the HTTP request decorators @Get(), @Put(), and @Delete() to define dynamic paths. Consequently, this route parameter lets you update, delete, and retrieve a single record.

Using the @Param() decorator, you can pluck properties from the params property of the request object and access the value of id:

@Get(':id')
async find(@Param('id') id: number): Promise<Item> {
  return this.itemsService.find(id);
}

@Params() and @Body() are both decorators that extract data from the request object.

Test the POST items/ endpoint by executing the following command:

curl -X POST -H 'Content-Type: application/json' -d '{
  "item": {
    "name": "Salad",
    "price": 4.99,
    "description": "Fresh",
    "image": "https://cdn.auth0.com/blog/whatabyte/salad-sm.png"
  }
}' http://localhost:7000/items -i

The -i flag includes protocol headers in the output.

You should get HTTP/1.1 201 Created as response.

Verify that an item named "Salad" has been added to the store:

curl http://localhost:7000/items -i

Enable Data Validation in NestJS

As explained before, using classes to create custom types is recommended since they are persisted as JavaScript entities after TypeScript transpilation. During development, TypeScript's type system will help you catch errors and debug your code. However, during production, your API will face unpredictable request payloads. You can mitigate this point of failure by validating the data it receives as payload.

To validate incoming requests, you can add validators to your Item class properties and execute them by using the built-in NestJS validation pipe.

As it is, the application won't return an error status if you POST invalid data. For example, the following request should return a 400 Bad Request response status; but instead, it returns a 201 Created response status:

curl -X POST -H 'Content-Type: application/json' -d '{
  "item": {
    "name": 1.99,
    "price": "Salad",
    "description": "Fresh",
    "image": "https://cdn.auth0.com/blog/whatabyte/salad-sm.png"
  }
}' http://localhost:7000/items -i

Output:

HTTP/1.1 201 Created
X-Powered-By: Express
Date: Fri, 20 Sep 2019 23:48:46 GMT
Connection: keep-alive
Content-Length: 0

To solve this problem, you'll set up your app to automatically validate all incoming requests using the built-in ValidatorPipe. In NestJS, a pipe is a class used to transform input data to a desired output or to validate input data.

To enable auto-validation, install the following NPM packages that are leveraged by ValidationPipe:

npm install class-validator class-transformer

class-validator makes validation easy by using TypeScript decorators. class-transformer offers proper decorator-based transformation, serialization, and deserialization of plain JavaScript objects to class constructors.

Once that's installed, update src/main.ts as follows:

// src/main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as dotenv from 'dotenv';
import { ValidationPipe } from '@nestjs/common';

dotenv.config();

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(process.env.PORT);
}
bootstrap();

Next, you need to add validation decorators to your Item class. Update src/item.ts like so:

// src/item.ts

import { IsString, IsNumber, IsOptional } from 'class-validator';

export class Item {
  @IsNumber() @IsOptional() readonly id: number;
  @IsString() readonly name: string;
  @IsNumber() readonly price: number;
  @IsString() readonly description: string;
  @IsString() readonly image: string;
}

In essence, since you expect the payload to your endpoints to have the same structure of an Item object, the Item class is doubling as a type class and a data transfer object (DTO) class. As requirements change, you may need to create a separate class to define a data transfer object for a payload that includes more data than what the Item class provides. Increasing the data present in the payload is done to reduce the number of method calls needed for your application to fulfill a request.

The id property is marked as optional since the POST items/ endpoint doesn't require an id as a route parameter or a payload property. Remember that the id of a new Item is created automatically by the create method of ItemsService.

Using this pipe, if the data is invalid, you throw an exception and return an error message to the client.

To test this, issue the following invalid POST request:

curl -X POST -H 'Content-Type: application/json' -d '{
  "item": {
    "name": 1.99,
    "price": "Salad",
    "description": "Fresh",
    "image": "https://cdn.auth0.com/blog/whatabyte/salad-sm.png"
  }
}' http://localhost:7000/items -i

This time, you do get a 400 Bad Request response. The message property of the response object also reveals the payload type errors:

{
  "statusCode": 400,
  "error": "Bad Request",
  "message": [
    {
      "target": {},
      "property": "name",
      "children": [],
      "constraints": {
        "isString": "name must be a string"
      }
    },
    {
      "target": {},
      "property": "price",
      "children": [],
      "constraints": {
        "isNumber": "price must be a number"
      }
    }
  ]
}

You can disable detailed validation error messages by passing an options object to the constructor of the ValidationPipe pipe if you need to:

// Extract from src/main.ts

app.useGlobalPipes(
  new ValidationPipe({
    disableErrorMessages: true,
  }),
);

With error messages disabled, the response to an invalid payload looks like this:

{"statusCode":400,"error":"Bad Request"}

Next Step: I've created endpoints with validation for my NestJS app

I ran into an issue

  • Twitter icon
  • LinkedIn icon
  • Faceboook icon