developers

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.

Oct 29, 20199 min read

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 appI ran into an issue