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); }
and@Params()
are both decorators that extract data from the@Body()
object.request
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
flag includes protocol headers in the output.-i
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
makes validation easy by using TypeScript decorators. class-validator
offers proper decorator-based transformation, serialization, and deserialization of plain JavaScript objects to class constructors.class-transformer
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:
Next Step: I've created endpoints with validation for my NestJS appI ran into an issue{"statusCode":400,"error":"Bad Request"}
About the author
Dan Arias
Staff Developer Advocate
The majority of my engineering work revolves around AWS, React, and Node, but my research and content development involves a wide range of topics such as Golang, performance, and cryptography. Additionally, I am one of the core maintainers of this blog. Running a blog at scale with over 600,000 unique visitors per month is quite challenging!
I was an Auth0 customer before I became an employee, and I've always loved how much easier it is to implement authentication with Auth0. Curious to try it out? Sign up for a free account ⚡️.View profile