1. |
|
---|---|
2. |
|
3. |
|
4. |
|
5. |
|
6. |
|
7. |
|
8. |
|
9. |
|
10. |
|
11. |
|
12. |
|
13. |
Controllers are one of the most important building blocks of NestJS applications as they handle request.
Let’s generate a Controllers with Nest CLI by running
$ nest generate controller
//or
$ nest g co
Since were working with our amazing new app called iluvecoffe
, let’s call our
first controller coffees
$ nest g co
? What name would you like to use for the controller? coffees
CREATE src/coffees/coffees.controller.spec.ts (499 bytes)
CREATE src/coffees/coffees.controller.ts (103 bytes)
UPDATE src/app.module.ts (340 bytes)
As we can see in our terminal, Nest automatically created a Controller and
a corresponding test file (.coffees.controller.spec.ts
) for us.
Also, we can see it updated module in our AppModule
. If we open up the app.module.ts
,
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { CoffeesController } from './coffees/coffees.controller';
@Module({
imports: [],
controllers: [AppController, CoffeesController],
providers: [AppService],
})
export class AppModule {}
We’ll see that the CLI automatically added this new CoffeesController
to the
controller:[]
array.
Note, that if we didn’t want to generate a test file, we could have simply
passed the --no-spec
flag like so.
$ nest g co --no-spec
Now, taking a look back at our project structure, we can see the files we just
created ended up in a directory based on the name we selected, in our case:
/src/coffees/
├── nest-cli.json
├── package.json
├── package-lock.json
├── README.md
├── src
│ ├── app.controller.spec.ts
│ ├── app.controller.ts
│ ├── app.module.ts
│ ├── app.service.ts
│ ├── coffees <<<
│ │ ├── coffees.controller.spec.ts <<<
│ │ └── coffees.controller.ts <<<
│ └── main.ts
├── test
│ ├── app.e2e-spec.ts
│ └── jest-e2e.json
├── tsconfig.build.json
└── tsconfig.json
If we want to generate something within a specific folder, just type in the
directory or directories which slashes /
prior to the Controllers name,
$ nest g co --no-spec modules/abc
For example, Nest generate Controllers module/abc
, will be placed within /src/module/abc
.
If you are not sure if the generator will place the file in the right directory,
use --dry-run
flag to see the simulated output form the CLI,
$ nest generate controller modules/abc --dry-run
CREATE src/modules/abc/abc.controller.spec.ts (471 bytes)
CREATE src/modules/abc/abc.controller.ts (95 bytes)
UPDATE src/app.module.ts (417 bytes)
This won’t actually create any files. So it’s a perfect way of testing any command to see what it will do, and where it might place things.
Back to our newly created CoffeesController
.
import { Controller } from '@nestjs/common';
@Controller('coffees')
export class CoffeesController {}
As we previously saw, the basic building blocks of Controllers in NestJS are classes and decorators.
We know that Controllers are something that handle request in our application. But how does the application know which URL accesses 'which' Controller?
You might already spotted it here, but the @Controller()
decorator, can be
passed a String. This String then passed the metadata needed for Nest to
create a routing map. Tying[1] incoming request to this
corresponding controller. In the case of our CoffeesController
, we can see
it has the string of 'coffees'
passed to the decorator. Tying the /coffees
URL for our application to this controller.
If we open up insomnia
or postman
and make request a 'GET' request to
http://localhost:3002/coffees
, we are going to see `404`-error.
{
"statusCode": 404,
"message": "Cannot GET /coffees",
"error": "Not Found"
}
We have the Controller setup, but it’s empty; and as the error message is hinting to us "Cannot GET /coffees"; We haven’t actually set up a 'GET' route in this Controller just yet!.
Lucky for us, Nest has decorators for all the common HTTP verbs, all
includes in the @nestjs/common
package, making this as easy as can be.
import { Controller, Get } from '@nestjs/common';
@Controller('coffees')
export class CoffeesController {
@Get() //[2]
findAll() { //[1]
return "This action returns all the coffees."; //[3]
}
}
Inside our CoffeesController
, let’s create a 'GET HTTP'-handler, using one
of the Nest decorators. Start by creating a method inside the controller. The
name of the method itself doesn’t matter, but let’s called findAll()
; as this
request, will be use to fetch all the result for this controller.
Now, let’s decorate this method with the @GET()
decorator; Make sure to import
it from @nestjs/common
.
For now, let’s just add a quick return
statement and echo some text back.
Let’s return a string that says something like, "This action returns all the coffees."
Within just like above, we mapped our first GET requests within the /coffees
route.
Other HTTP verbs will be done in the same fashion; and it placed inside of
this Controller, they would be mapped to /coffees
as well. Let’s save our
progress and see if we can access this GET-route from insomnia
or postman
.
Great, it works perfectly!. We can see that we got: "This action returns all the coffees." back from the API just like we expected.
Now, what if we wanted to have a nested URL for this specific GET-request? Just as we saw with Controllers, all of the HTTP decorator’s take one parameter, a String; which create a "nested-path" and appends it to the one included form the controller itself.
import { Controller, Get } from '@nestjs/common';
@Controller('coffees')
export class CoffeesController {
@Get('flavors') //<<<<
findAll() {
return "This action returns all the coffees.";
}
}
If we updated our GET request, to @GET('flavors')
, we can now access this
route via /coffees/flavors
. Let’s save our changes, and head back to
insomnia
and hit this new endpoint /coffees/flavors
. Perfect, the routes
works at its new nested URL.
Everything we’ve shown so far gives us amazing control and flexibility over our HTTP verbs. Making them easy to read, and uniform throughout our application.
Routes with specific paths wont’s works when you need to accept dynamic data
as part of you request. Let’s say we made a GET request to /coffees/123
, where
123
us dynamic and referring to an ID
. In order to define routes with
parameters, we can add root parameters tokens to the path of the routes.
This lets us capture these dynamic values at that position in the request-URL, and passed them into the method as parameter.
Let’s learn how all of this works by creating a new endpoint in our
CoffeesController
for this exact scenario.
import { Controller, Get, Param } from '@nestjs/common';
@Controller('coffees')
export class CoffeesController {
@Get('flavors')
findAll() {
return "This action returns all the coffees.";
}
@Get(':id')
findOne(@Param() id) {
return "This action returns #${params.id} the coffees.";
}
}
Let’s create a method called findOne()
, and add the Nest @Get()
decorator on
top. This time, let’s pass in :id
inside of the @Get()
decorator. This
signifies that we’re expecting a dynamic root parameter named "id".
Next, let’s go inside of the findOne()
parameters and use a new Nest decorator
called @Param()
, also form @nest/common
and name it “params”. The
@Param()
decorator let us grab all incoming request parameters and use them
inside of the function body of our method.
When we don’t pass anything inside of the @Param()
decorator, we receive all
request parameters, letting us access ${params.id}
from the object.
import { Controller, Get, Param } from '@nestjs/common';
@Controller('coffees')
export class CoffeesController {
@Get('flavors')
findAll() {
return "This action returns all the coffees.";
}
@Get(':id')
findOne(@Param('id') id: string) {
return "This action returns #${params.id} the coffees.";
}
}
Sometimes, we don’t want to access the entire params objects. With the
@Params()
decorator, we have the options of passing in a String inside of it,
to access a specific portion of the params. Let’s enter in 'id'
directly
inside of the decorator. But let’s make sure we update our @Param()
name to
id:
of type String to reflect these changes.
Let’s save everything, head over to insomnia
and access, GET /coffees/123
,
to see if it’s able to grab '123'
from the URL. If we changes this to 10
, we
can see that it’s entirely dynamic picking up any number we pass in.
In this lesson, let’s look at how we can work with POST-request and retrieve request payloads that are typically passed alongside them.
Similar to the @Param()
decorator we just learned about, Nest also has
a helpful decorator for getting all or specific portions of the request.body
know as the @Body()
decorator.
import { Controller, Get, Param, Post, Body } from '@nestjs/common';
@Controller('coffees')
export class CoffeesController {
@Get('flavors')
findAll() {
return "This action returns all the coffees.";
}
@Get(':id')
findOne(@Param('id') id: string) {
return `This action returns #${id} the coffees.`;
}
@Post()
create(@Body() body) { // <<<
return body;
}
}
Let’s add new create()
POST method to our CoffeesController
, making sure we
import both the @Post()
and @Body()
decorators from @nestjs/common
.
Notice: we’re using the @Body()
decorator in our method parameters, just like
we did with @Param()'s
.
To testing if everything’s working, let’s return body
in our method, so we can
if the payload[2] comes back with the response.
Back to `insomnia. Let’s execute POST request to http://localhost:300/coffees, and pass in some arbitrary key/values for the request body by selecting JSON as our payload format.
We’re going to pass in any sort of JSON shape, so enter whatever you fill like.
{
"name": "Old Florida Roast",
"brand": "Salemba Brew"
}
As we can see, the request Body is automatically accessed from within our endpoint method!.
Sometimes we don’t want to access entire body. If we want to access just
a specific portion of it, we can actually pass in a String to the
@Body(/* String here */)
decorator, just like we do with @Param()
.
import { Controller, Get, Param, Post, Body } from '@nestjs/common';
@Controller('coffees')
export class CoffeesController {
@Get('flavors')
findAll() {
return "This action returns all the coffees.";
}
@Get(':id')
findOne(@Param('id') id: string) {
return `This action returns #${id} the coffees.`;
}
@Post()
create(@Body('name') body) { // <<<
return body;
}
}
Let’s test it out by adding the String 'name'
inside and save our changes.
Back over to insomnia
. Let’s hit the endpoint again. But this time we’ll see
that we only get the name
value returned. It worked!.
When using this approach, just keep in mind that you may run into potential validation issues by doing this. Because if we access ta specific properties, other properties WON’T be validated. So, use this with CAUTION.
Let’s revert these changes and remove name
from our @Body()
decorator.
Let’s save everything and test the endpoint again in insomnia
, just to make
sure.
Great, we see the entire Body response being passed back again!.
You might have notice that all the API request we’ve made so far, when we
they’re successful.., automatically sent back status code: 200
fort GET and
201
for POST. But we never set any of that up!.
Well, Nest actually servers back these code by default for successful request. But let’s a look at a few ways we can customize and send back whatever codes we need for any given scenario.
One simple way to statically change this behavior is by adding an @HTTPCode()
decorator that will see in a moment at the handler level.
To illustrate this with an example. Let’s say we wanted to deprecate our POST
requests and pass the 410 - GONE
HTTP status code back to anyone hitting
the endpoint. Nest also includes a helpful Enum we’ll be using called
HttpStatus
, so we don’t have to memorize all the status code numbers.
In our CoffeesController
, let’s learn how to put all this into action by
applying it to the POST endpoint we just created in the previous lesson. Above
create()
method let’s add another decorator and import @HttpCode()
from
@nestjs/common
import { Controller, Get, Param, Post, Body, HttpCode, HttpStatus } from '@nestjs/common';
@Controller('coffees')
export class CoffeesController {
@Get('flavors')
findAll() {
return "This action returns all the coffees.";
}
@Get(':id')
findOne(@Param('id') id: string) {
return `This action returns #${id} the coffees.`;
}
@Post()
@HttpCode(HttpStatus.GONE) // <<<
create(@Body() body) {
return body;
}
}
This decorator allows us to set a specific status code for the entire
response. Inisde the parentheses, let’s pass in HttpStatus
, importing from
@nestjs/common
as well. When we type period .
after it, we could see of the
available HTTP status code available to us.
Let’s select GONE
, and save our changes. Open up insomnia
and hit this POST
endpoint to see what we get as response now.
// request 'POST - http://localhost:3002/coffees'
// response, 401 - GONE //<<<
We can see that we recieved 410 - GONE
back from the request now, perfect!.
This decorator we used @HttpStatus()
is useful when the status code is
static. But when we dive deeper into handling errors in later chapters. We’ll
look at other helpers methods and utilize Nest provides to give us even more
control.
With Nest, we also have the option of using the underlying library specific response object that are application is using. By default NestJS is using ExpressJS under the hood.
But as we know, our applications could be switched to use Fastify if we wanted as well.
To access these underlying response objects, Nest has a decorator called
@Res()
[3]. The @Res()
decorator can be used within an endpoint method
parameters, letting us use the native response handling method exposed by
the library.
To learn how to use this, let’s open our CoffeesController
and make some
changes to findAll()
GET method.
import { Controller, Get, Param, Post, Body, HttpCode, HttpStatus, Res } from '@nestjs/common';
@Controller('coffees')
export class CoffeesController {
@Get('flavors')
findAll(@Res() response) {
response.status(200).send(This action returns all the coffees.);.
// return "This action returns all the coffees.";
}
}
First, let’s import @Res()
decorator here from @nestjs/common
and name this
parameter response
. Since Nest is using ExpressJS by default, and so is our
application, we can utilize any method standard to the ExpressJS library with
this parameter.
To use these native ExpressJS methods, let’s remove our return
line and
replace it with response
, calling the status method on it. Passing in 200
;
and lastly, let’s call the send()
method passing in the String we were already
return.
If we save our changes, and head back to insomnia
let’s hit the GET endpoint
for /coffees
just to make sure everything’s still working.
// request 'GET - http://localhost:3002/coffees'
// response, 200 - OK //<<<
Perfect, we’re getting the same response and it’s still showing a 200
status
code, great!.
As a word of CAUTION!!, although this approach works great and does allow for a little more flexibility in some ways by providing full control of these response object. Like header manipulation, library specific features and so on, it SHOULD BE USED WITH CARE.
In general, your approach is much less clear and does have some disadvantages.
Some main disadvantages of this approach, are that you lose compatibility with
Nest features that depend on Nest standard response handling, such as:
interceptors[4] and the @HttpCode()
decorator.
When we use the underlying library response like this, our code can become platform dependent[5] as different libraries might different APIs on the response object.
Using this native response also makes our code harder to test, since we’ll have to mock the response object as well.
As a best practice it is recommended to us the Nest standard approach when dealing with response whenever possible.
Let’s make sure we revert all of these changes we made in this chapter, saving it everything before continuing on to the next chapter.
So far in this course, we’ve only made handlers for create and read operations. In this lesson.Let’s look at how we can handle other common operations like UPDATE and DELETE.
There are two different HTTP methods we can use for UPDATE, PUT
and
PATCH
.
A PUT
operation replaces the entire resource, because of this we need to
have the entire object within the request payload.
A PATCH
operation is different. In that, it only modifies a resource
partially allowing us to update even just a single property of a resource if
we’d like.
Let’s add a PATCH endpoint to our CoffeesController
to see it in action.
import { Controller, Get, Param, Body, Post, Patch, Put, Delete } from '@nestjs/common';
@Controller('coffees')
export class CoffeesController {
@Get()
findAll() {
return "This action returns all the coffees.";
}
@Get(':id')
findOne(@Param('id') id: string) {
return `This action returns #${id} the coffees.`;
}
@Post()
@HttpCode(HttpStatus.GONE)
create(@Body() body) {
return body;
}
@Patch(':id')
update(@Param('id') id: string, @Body() body) {
return `This action returns #${id} the coffees.`;
}
@Delete(':id')
remove(@Param('id') id: string) {
return `This action removes #${id} coffee`;
}
}
To get started. Let’s make a new method on our controller and call it
update()
, making sure to add the @Patch()
decorator on top. Inside of the
@Patch()
decorator Let’s make sure to pass in the String of ':id'
indicate
what coffee we’re going to update.
Since a @Patch()
operation does a partial update of a single resource. It
requires both an id
and a payload
representing all of the possible values
for a given resource. For this we need to take advantage of both @Param()
and @Body()
decorators.
Let’s jump into the method signature and grab the incoming request parameters
via our @Params()
decorator, passing in 'id'
inside of it naming this
parameter id:
, which is of course type String.
Our second parameter is going to be the request body. So let’s grab it via
the @Body()
decorator and let’s call this parameter body
for now.
You can see that we are passing in both the `id` param to indicate what entity to update, and the request payload that we’ll use to update that existing resource with.
Let’s just add a little String return statement so we can test if everything’s working so far, great.
Next. Let’s look at the DELETE operation.
Let’s create a new method in our CoffeesController
and call it remove()
with
the @Delete()
decorator on top also from @nestjs/common. Just like
`@Patch()
we need to make sure we’re passing in an :id
for delete operations
so we can indicate which exact item needs to be deleted.
Just like before in the parameters of this method. Let’s make sure to utilize
The @Param()
decorator so that we can grab the 'id'
from the incoming
request. Lastly let’s return
a String just like before that says something
like, This action removes #${id} coffee
.
Let’s save all of our changes and see if everything works.
Now if we open up insomnia
, and do a PATCH request for /coffee/123
, 123
being an id
, with the Body as JSON format,
{
"name": "Old Florida Roast",
"brand": "Salemba Brew"
}
We should see, This action updates #123 coffee
.
Let’s change the method to DELETE request, and push send again. This time we
should see This action removes #123 coffee.
In most cases applications, we need to be able to interact with large data sets. For example, let’s imagine our database’s `coffee" table has 'every' brand of coffee on the planet!. Without pagination, a simple search for all coffees could return millions of rows over the network. This is exactly where pagination comes in!.
With pagination, we can split this massive data response into manageable
chunks or pages, returning only what’s really needed for each specific response.
Whether that’s 10
, 50
, 100
or however many result we want, with each one
of those responses.
As a best practice, we want to use PATH parameter to identify a specific resource while using query parameters to filter or sort that resource.
Nest has a helpful decorator for getting all or a specific portion of the query
parameters called @Query()
. Which works similar to @Param()
and @Body()
,
which we’ve already seen. Let’s modified findAll()
method, and put the new
@Query()
decorator to use.
import { Controller, Get, Param, Body, Post, Patch, Put, Delete, Query } from '@nestjs/common';
@Controller('coffees')
export class CoffeesController {
@Get()
findAll(@Query() paginationQuery) {
const { limit, offset } = paginationQuery;
return `This action returns all the coffees. Limit ${limit}, offset: ${offset}`;
}
...
...
}
Inside our findAll()
method, let`s add a parameter called paginationQuery
and decorate it with the @Query()
decorator importing it form @nestjs/common
.
Now, inside our method, let’s take advantage of object destructuring to get
limit
and offset
from paginationQuery
. Both of which we will be expecting
to come in with every request. We don’t have a Type for this parameter yet,
but don’t worry, we’ll be covering that in later videos.
Now, for testing purposes, let’s make some changes to our return
statement
here, so that it send back these limit
and offset
variables with the
response.
Now, let’s make sure we save all of our changes, and head back over to
insomnia
.
Back in insomnia
, let’s execute a GET request to this updated /coffee
endpoint and pass limit
and offset
as part of the URL as query parameters.
For example, we’re going to pass in limit=20
and offset=10
.
// request 'GET - http://localhost:3002/coffees?limit=20&0ffset=10'
// response, 200 OK
This action return all the coffee. Limit 20, offset: 10
If we make the request, our response should comeback with, "This action return all the coffee. Limit 20, offset: 10" - just as we expected!.
Services are very important parts of Nest applications. As they help us separate our Business logic from our Controller. Separating our business logic into Services, helps make this logic reusable throughout multiple parts of our applications.
To create a Service using the Nest CLI - simply enter:
$ nest generate service
// or
$ nest g s
Let’s isolate our Coffees business logic and create a CoffeesService
for our
application.
$ nest g s
? What name would you like to use for the service? coffees
CREATE src/coffees/coffees.service.spec.ts (467 bytes)
CREATE src/coffees/coffees.service.ts (91 bytes)
UPDATE src/app.module.ts (416 bytes)
So, when generating our Service, let’s enter "coffees" for the name. The CLI
will generate a service and a corresponding test file, as well as
automatically including this services to the providers:[]
Arrays of the
closest Module.
In NestJS, each service is a "providers". But what do we mean by a "provider"[6]?
Well, the main idea of a provider is, that it can inject dependencies. This means that objects can create various relationship to each other, and the logic of wiring up instances of objects together, can all be handled by the Nest runtime-system, as opposed to trying to create and manage this type of dependencies injection[7] yourself.
//coffes.services.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class CoffeesService {}
So, what do these providers look in Nest? Well, like other things we’ve seen in
Nest, providers are just a class annotated with a decorator called @Injectable()
.
Our CoffeesService
that we just created, will be responsible for data
storage and retrieval[8]; and is designed to be used by the CoffeesController
or
anything else that might need this functionality.
So, how can weuse dependency injection in Nest? Well, to inject the provider, we can simply use constructors!.
Let’s open up our CoffeesController
and define a constructor()
.
// coffee.controller.ts
import { Controller, Get, Param, Body, Post, HttpCode, HttpStatus, Res, Patch, Delete, Query } from '@nestjs/common';
@Controller('coffees')
export class CoffeesController {
constructor() { // <<<
}
@Get()
findAll(@Query() paginationQuery) {
const { limit, offset } = paginationQuery;
return `This action returns all the coffees. Limit ${limit}, offset: ${offset}`;
}
@Get(':id')
findOne(@Param('id') id: string) {
return `This action returns #${id} the coffees.`;
}
@Post()
@HttpCode(HttpStatus.GONE)
create(@Body() body) {
return body;
}
@Patch(':id')
update(@Param('id') id: string, @Body() body) {
return `This action returns #${id} the coffees.`;
}
@Delete(':id')
remove(@Param('id') id: string) {
return `This action removes #${id} coffee`;
}
}
Nest handled dependency injection for us. This is achieved by looking at the Type of whatever we pass into a constructor’s parameters.
Let’s try this out by injecting our CoffeesService
right here. Let’s start by typing in:
constructor(private readonly coffeesService: CoffeesService) {}
All right, so let’s break everything down, so that we understand each piece here.
First, notice the use of the private
access modifier syntax here.
These TypeScript shorthand allows us to both declare and initialize the
CoffeesService
member immediately in the same location. As well as making it
only accessible within the class itself, hence "private".
Next, we utilized the keyword "readonly". This is more so a best practice, but this helps us ensure that we aren’t modifying the service referenced, and in fact, only accessing things from it.
Next, we are simply naming our parameter here, calling it coffeesService
, just
to make it very clear, and readable for others.
In Nest, thanks to TypeScript capabilities, it’s extremely east to manage
dependencies, because they are resolved simply by their Type. This why we have
:CoffeesService
. Nest will resolve the CoffeesService
by creating and
returning an instance of CoffeesService
to our CoffeesService
; or in the
normal case of singleton[9], returning the-existing-instance if it
has already been requested elsewhere. This dependencies is resolved and passed
to your controllers constructor or assigned to the indicated property here.
Now, that we have our dependency set up, let’s shift our focus back to or
CoffeesService
itself. Typically in applications, providers and
services handle business logic as well as interactions with data
source.
We’re going to keep things simple things for now, and work with a property
within our CoffeesService
that can obtain some mock data.
Let’s open up our CoffeesService
| coffee.service.ts
file, and add
a "coffees" property, which we’ll pretend is our data source. Don’t worry,
we’ll be working with real database in the future lesson.
// coffee.service.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class CoffeesService {
private coffees = [];
}
For now, let’s use this 'Array of coffees' as our "databases", to READ, UPDATE, and DELETE items from. But let’s take it up a notch and create a "Resource Entity" for these items so that we know what Type they are.
First, Let’s create an /entities
directory in our /src/coffees/
folder.
Inside of the /entities
folder, let’s create a new file called
"coffee.entity.ts".
Since this is going to take the shape of our resource entity, let’s add a few properties to make it feel like something we’d get from a real database.
// coffee.entitiy.ts
export class Coffee {
id: number;
name: string;
brand: string;
flavors: string[];
}
Let’s create and export a class named Coffee
and give it several properties,
'id'
of Type Number, 'name'
and 'brand'
, both of Type String, and
lastly, 'flavors'
which we will make an string[]
(Array of String).
Let’s go back to CoffeesService
and update our resource, to utilize this new
Entity Class.
// coffee.service.ts
import { Injectable } from '@nestjs/common';
import { Coffee } from './entities/coffee.entity';
@Injectable()
export class CoffeesService {
private coffees: Coffee[] = [
{
id: 1,
name: 'Salemba Roast',
brand: 'Salemba Brand',
flavors: ['chocolate', 'vanilla'],
},
];
}
So, let’s make sure we set coffees
to Type Array of Coffees. We can also
predefine a single Entity within the Array, just for testing purposes. Now, that
our pseudo data-source is set up, let’s create some CRUD operations around this,
to bring some life and business logic to our CoffeesService
.
When we say CRUD operations, we’re talking about the big four: Create, Read, Update, and Delete.
As, this would be a lot of code to go through, we’re going to paste in a full
implementation of everything. But, if you’re following along at the bottom of
this lesson plan, you’ll see the same fully populated CoffeesService
you can
use in your application.
// coffee.service.ts
import { Injectable } from '@nestjs/common';
import { Coffee } from './entities/coffee.entity';
@Injectable()
export class CoffeesService {
private coffees: Coffee[] = [
{
id: 1,
name: 'Salemba Roast',
brand: 'Salemba Brand',
flavors: ['chocolate', 'vanilla'],
},
];
findAll() {
return this.coffees;
}
findOne(id: string) {
return this.coffees.find(item => item.id === +id);
}
create(createCoffeeDto: any) {
this.coffees.push(createCoffeeDto);
}
update(id: string, updateCoffeeDto: any) {
const existingCoffee = this.findOne(ind);
if (existingCoffee) {
// update the existing entity
}
}
remove(id: string) {
const coffeeIndex = this.coffees.findIndex(item => item.id === +id);
if (coffeeIndex >= 0) {
this.coffees.splice(coffeeIndex, 1);
}
}
}
In above code, we can see we’ve added interactions with our data-source that
help us, findAll()
, findOne()
, create()
, update()
, and remove()
Coffees. These are, of course, sample implementation "without" a real
database. But you got the idea.
Services are where the meat of our business logic should be held, along with any interactions with data-source.
Now that we have everything setup, let’s pop back over to our
CoffeesController
and utilize this new methods.
import { Controller, Get, Param, Body, Post, HttpCode, HttpStatus, Res, Patch, Delete, Query } from '@nestjs/common';
import { CoffeesService }from './coffees.service';
@Controller('coffees')
export class CoffeesController {
constructor(private readonly coffeesService: CoffeesService) {}
@Get()
findAll(@Query() paginationQuery) {
const { limit, offset } = paginationQuery;
return this.coffeesService.findAll();
// return `This action returns all the coffees. Limit ${limit}, offset: ${offset}`;
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.coffeesService.findOne(id);
// return `This action returns #${id} the coffees.`;
}
@Post()
@HttpCode(HttpStatus.GONE)
create(@Body() body) {
return this.coffeesService.create(body);
// return body;
}
@Patch(':id')
update(@Param('id') id: string, @Body() body) {
return this.coffeesService.update(id, body);
// return `This action returns #${id} the coffees.`;
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.coffeesService.delete(id);
// return `This action removes #${id} coffee`;
}
}
Let’s replace all of our empty methods to utilize our CoffeesService
and call
the relevant method.
For each method in our controller let’s remove the mock return
String and
instead call the corresponding CoffeesService
making sure the pass in the
necessary parameters.
return this.coffeesService.findAll();
For the findAll()
method, let’s ignore the pagination for the time being.
We’ll be using those in the future lesson.
return this.coffeesService.findOne(id);
For findOne()
controller. Let’s call, findOne()
method from CoffeesService
and make sure to pass in our 'id'
.
return this.coffeesService.create(body);
For the create()
method on controller, let’s call create()
method from
CoffeesService
and pass in 'body'
return this.coffeesService.update(id, body);
For update()
method on controller, let’s call update()
method form
CoffeesService
passing in both parameters 'id'
and 'body'
return this.coffeesService.remove(id);
Last but not least, for remove()
method on controller, let’s call the
remove()
method on CoffeesService
and pass down the 'id'
.
With everything in place, let’s save our progress and see if our provider gets called from the routes we just updates.
Now, over an insomnia
, let’s test some of these endpoints to make sure
everything is wired up properly.
First, let’s try a GET request to /coffees/1
, which is our findOne()
method.
// request 'GET - http://localhost:3002/coffees/1'
// response, 200 OK
{
"id": 1,
"name": "Salemba Roast",
"brand": "Salemba Brand",
"flavors": ["chocolate", "vanilla"]
}
Great, we’ve got data back!.
Now, let’s test to findAll()
method, and make a GET request for /coffees
,
// request 'GET - http://localhost:3002/coffees'
// response, 200 OK
{
"id": 1,
"name": "Salemba Roast",
"brand": "Salemba Brand",
"flavors": ["chocolate", "vanilla"]
}
We should get an Array back of all these coffees. So far so good.
Next, let’s test the delete()
functionality and make a DELETE - request to
/coffees/1
// request 'DELETE - http://localhost:3002/coffees/1'
// response, 200 OK
No body returned for response
Great, we’ve got 200
back, it’s working too!.
Lastly, let’s make sure that the coffee is really gone and make a GET
- request for ID 1
again, to /coffees/
// request 'GET - http://localhost:3002/coffees'
// response, 200 OK
No body returned for response
Perfect, we got an empty Array back, it really got deleted, our CoffeesService
completely works!.
So far, we’ve looked at when everything goes right in our applications. But what about when we get applications errors? What if an API request fail or times-out? What if the database cannot find the resource we’re looking for?
A lot things can go wrong in complex applications!. But lucky for us, Nest can
help easily send back any type of user friendly error message we want. With Nest
we’ve few options to choose form: [1]
Throwing an Exception, [2]
Using
library specific response objects; We can [3]
even create "Interceptors" and
leverage "exception filters", which we’ll be covering later on in this course.
Let’s see an HTTpException
in action by opening up our CoffeesService
and
applying it within our findOne()
method. Throwing a different status code for
a very common scenario.
// coffees.service.ts import { Injectable, HttpException } from '@nestjs/common'; import { Coffee } from './entities/coffee.entity'; @Injectable() export class CoffeesService { .... .... findOne(id: string) { const coffee = this.coffees.find(item => item.id === +id); if (!coffee) { throw new HttpException(`Coffee #${id} not found`, HttpStatus.NOT_FOUND); } return coffee; } }
In this scenario, let’s say w want to throw an Error whenever the user tries to
fetch a coffee, that DOESN’T exist in our data-source. Let’s start by assigning
the value coming back from coffees.find()
here to a variable and naming
coffee
.
Next, let’s add an 'if'
statement, for when coffee
is not defined. Inside
our 'if'
, let’s add throw new HttpException()
importing it from
@nestjs/common
. HttpException()
here take two parameters: [1]
One being
a String for the Error response message, [2]
and the other being: the
Status Code we want to send back.
For our Error mesage, let’s just pass "Coffee #${id} not found". For our
status-code, let’s use the HttpStatus
utility Enum, we used in a previous
chapter. So we don’t look up or memorize all the different status code, and
select: NOT_FOUND
.
After our if statement, we know that w have a coffee
, so let’s just return it
back with the response.
If we save our changes, open insomnia
and try to make a GET - request to
/coffees/2
, for example,
// request 'GET - http://localhost:3002/coffees/2'
// response, 404 NOT FOUND
{
"statusCode": 404,
"message": "Coffee #2 not found"
}
We’ll see that our code worked, and the response came back with a 404
status
code. Since there is NO coffee
with the 'id'
of 2
.
Note that Nest also has helper methods for all of the common error responses[10].
These are useful and you know exactly which could you need to send back and
prefer a simpler and more readable approach. These include helper classes like:
NotFoundException
, InternalServerErrorException
, BadRequestExpection
, and
many more.
Let’s clean up the HttpException
we just made, and pass back one of these
simplified ones.
// coffees.service.ts
import { Injectable, HttpException, NotFoundException } from '@nestjs/common';
import { Coffee } from './entities/coffee.entity';
@Injectable()
export class CoffeesService {
....
....
findOne(id: string) {
const coffee = this.coffees.find(item => item.id === +id);
if (!coffee) {
throw new NotFoundException(`Coffee #${id} not found`);
}
return coffee;
}
}
Let’s change our code from HttpException
and replace it with:
NotFoundException
also from @nestjs/common
, Let’s make sure to remove this
second parameter here HttpStatus.NOT_FOUND
, since we don’t need it anymore.
This helper already passes back the correct status-code for us.
// request 'GET - http://localhost:3002/coffees/2'
// response, 404 NOT FOUND
{
"statusCode": 404,
"message": "Coffee #2 not found",
"error": "Not Found"
}
If we hit the API again, we’ll see it passess a response with the same Error message.
But, what about scenarios where we forgot handle an exception in our application
code? Let’s say an exception that isn’t an HttpException
.
Well, luckliy, Nest aoutomatically catches these exception for us with a built in exception layer. This layer even send back an appropriate user friendly response for us.
// coffees.service.ts
import { Injectable, HttpException, NotFoundException } from '@nestjs/common';
import { Coffee } from './entities/coffee.entity';
@Injectable()
export class CoffeesService {
....
....
findOne(id: string) {
throw "A Random Error"
const coffee = this.coffees.find(item => item.id === +id);
if (!coffee) {
throw new NotFoundException(`Coffee #${id} not found`);
}
return coffee;
}
}
Let’s test this out by forcing a JavaScript error in our GET request here, and add a random "throw error" message. Let’s enter in a message like: "A Random Error";
If we head back to insomnia
and hit the endpoint again,
// request 'GET - http://localhost:3002/coffees/2'
// response, 500 Internal Server Error
{
"statusCode": 500,
"message": "Internal server error"
}
We’ll see Nest automatically will send back a 500
- Internal Server Error for us.
If we look at our terminal,
[Nest] 2242990 - 03/23/2021, 2:56:14 PM [ExceptionsHandler] A Random Error +3391ms
We could see the error-message we set outputting "A random Error". This is super helpful for that might be very deep within our code, or even third party library we may be using. This helps, make sure that every error "bubbles up", and all errors come through no matter what!.
In NestJS, Modules are strongly recommended as an effective way to organize your application components.
For most Nest application an ideal architecture should employ multiple modules, each encapsulating a closely related set of capabilities. To illustrate this with an example, let’s imagine that we were creating functionality around a Shopping Cart. If our applications has a Shopping Cart Controller, and Shopping Cart Service. Both of these belong to the same application domain as they are very closely related. This would be perfect example of when it might make sense to group parts of our application together and move them into their own feature module.
For iluvcoffee application, so far we’ve had everything in one big module.
Let’s encapsulate some of the work we’ve done so far with CoffeesController
and CoffeesService
and bring them together into their own CoffeesModule
.
To generate a Nest module with CLI simply run:
$: nest generate module
//or
$: nest g mo
Simply run nest g module
followed by the name of the module.
$ nest g mo
? What name would you like to use for the module? coffees
CREATE src/coffees/coffees.module.ts (84 bytes)
UPDATE src/app.module.ts (487 bytes)
Since we’re building a coffees - module. We’re going to type in coffees
for
our name and push enter.
// app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { CoffeesController } from './coffees/coffees.controller';
import { CoffeesService } from './coffees/coffees.service';
import { CoffeesModule } from './coffees/coffees.module'; // <<<
@Module({
imports: [CoffeesModule], // <<<
controllers: [AppController, CoffeesController],
providers: [AppService, CoffeesService],
})
export class AppModule {}
The CLI will now generate a module-class, and automatically add this module
to the imports:[]
Array of the closest module, the closest and only module in
our application is going to be our AppModule
. The CLI automatically added our
CoffeesModule
import here for us.
Okay, so let’s open up our newly-created CoffeesModule
/ coffees.module.ts
,
// coffees.module.ts
import { Module } from '@nestjs/common';
@Module({})
export class CoffeesModule {}
Looking at our empty CoffeesModule
, we can see that a NestJS Module is simply
a class annotated with the @Module()
decorator, this decorator provides
metadata that Nest uses to organized the application structure.
The @Module()
decorator takes a single Object whose properties describe the
module, and all of its context. Modules contain FOUR main things:
-
Controllers.
Which you can think of as our API - roots that we want this module to instantiate. -
Exports,
Here, we can list Providers within this current - module that should be available anywhere THIS module is "imported" -
Imports,
Just as we say inAppModule
. Theimports:[]
Array gives us the ability to list-other-modules that THIS module "requires" are now fully available HERE within this Module as well!. -
Providers,
With thisproviders:[]
Array. We’re going to list our Service that need to be instantiated by Nest@Injector()
. Any Providers here will be available only within THIS module itself, unless added to theexports:[]"
Array we saw above!.
Great. So now that we understand the basic concepts of Nest - Modules. Let’s
group some of our previous CoffeesController
, and CoffeesService
files to be
a part of this (CoffeesModule
) particular module.
This will help us practice grouping and modularizing our application functionality.
Let’s start by including these two files within our new CoffeesModule
.
// coffees.module.ts
import { Module } from '@nestjs/common';
import { CoffeesController } from './coffees.controller';
import { CoffeesService } from './coffees.service';
@Module({ // <<<
controller:[CoffeesController],
providers: [CoffeesService]
})
export class CoffeesModule {}
Inside of our @Module()
decorator, let’s first add the controllers: []
- property. This property expects an Array, and inside of the Array, let’s add
our CoffeesController
, by importing it from ./coffees.controller.ts
. Let’s
add providers:[]
which also an Array, and include our CoffeesService
here.
Perfect!. Something important to remember here, We originally had
CoffeesController
and CoffeesService
as part of our overall AppModule
. So
let’s make sure we remove those reference from the root - module
(app.module.ts
). This will help prevent us from running into any unexpected
issue as they would be instantiated TWICE if we didn’t do this!.
// app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { CoffeesModule } from './coffees/coffees.module';
@Module({
imports: [CoffeesModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
As we can see, a feature module simply organizes code relevant for a specific feature. Helping us keep code organized, and establishing clear boundaries for our application and its features. This also helps us manage complexity and develop with SOLID principles, especially as the size of the application or our team grows.
Let’s save our application so far, fire up some insomnia
and make sure
everything’s working still.
Excellent, everything still working as expected, and we’ve managed to encapsulate our coffee - logic into its own dedicated NestJS - Module.
A "Data Transfer Object", also known as a DTO. Is an Object that is used to encapsulate data and send it from one application to another. DTO’s helps us define the interfaces or input and output within our system.
For example, let’s imagine we have a POST - request, and with DTO’s we can define the shape or inteface for what we’re expecting to recieve for our Body.
So far in this course, we’ve used the @Body()
decorator in our POST and PATCH
endpoints. But we have no idea we’re expecting the payload to be. This is
exactly where DTO’s come in.
To generate a DTO, we can use the Nest - CLI to simply generate a basic class
for us via Nest generate class (followed by the name). To help tie everything
together. Let’s generate a CreateCoffeeDto
class for our POST endpoint within
our CoffeesController
.
$ nest g class coffees/dto/create-coffee.dto --no-spec
CREATE src/coffees/dto/create-coffee.dto.ts (32 bytes)
In our terminal, let’s generate this by entering nest g class
coffees/dto/create-coffee.dto
with --no-spec
flag at the end to avoid
generating a test file.
NoteAs side note, remember that we’re able to generate files inside of whatever folder we want, as long as we list the directory BEFORE the file name.
This is why the name of our file here is ALSO the directory. We also added
.dto
at the end, as a naming convention best practice. Allowing us to
quickly see and know what this file is.
The Nest - CLI is going to generate a plain class that we can use as our DTO
(Data Transfer Object). Just to keep things really clean and organized, we’ve
decided to create this file within a dedicated "/dto"
directory in our
/coffees
- folder. This is a great application convention to get into not only
for other DTO’s you might create, but you can similarly group your
Interfaces, Entities, and many other similar items in their own folders,
all grouped within their associated module.
TipWith this convention behavior on your belt, it’s will help keep your code even more organized, clean and easier to understand.
// create-coffee.dto.ts
export class CreateCoffeeDto {}
Looking at this newly created file, we’re going to be using CreateCoffeeDto
as
an EXPECTED Input Object shape for our CoffeesController
POST - request. This
DTO, will help us be able to do things, like make sure the request payload
has everything we require before running further code.
When we create a new - coffee in our application, what properties do we need to have here for this DTO?
Let’s look at our coffee.entity.ts
as an example for what our mock resource
looks like and what properties we might need.
// coffee.entity.ts
export class Coffee {
id: number;
name: string;
brand: string;
flavors: string[];
}
We won’t need to pass in an "id"
when creating a coffee. That’s something
would be generated by our database - once we set that up in future chapter. But
the rest of these properties here are perfect for our DTO.
Let’s go ahead and copy the name
, brand
, and flavors
and paste inside of
our CreateCoffeeDto
.
// create-coffee.dto.ts
export class CreateCoffeeDto {
name: string;
brand: string;
flavors: string[];
}
Now, that our DTO is all set, let’s save our changes and head over to the POST
- request within our CoffeesController
.
// coffee.controller.ts
import { Controller, Get, Param, Body, Post, HttpCode, HttpStatus, Res, Patch, Delete, Query } from '@nestjs/common';
import { CoffeesService }from './coffees.service';
@Controller('coffees')
export class CoffeesController {
...
...
@Post()
create(@Body() body) { // <<<
return this.coffeesService.create(body); // <<<
}
...
...
}
Right now, we’re using @Body()
here, but we don’t have a "Type" for our
payload. This is exactly where our new DTO comes in!.
// coffee.controller.ts
import { Controller, Get, Param, Body, Post, HttpCode, HttpStatus, Res, Patch, Delete, Query } from '@nestjs/common';
import { CoffeesService }from './coffees.service';
import { CreateCoffeeDto } from './dto/create-coffee.dto';
@Controller('coffees')
export class CoffeesController {
...
...
@Post()
create(@Body() CreateCoffeeDto: CreateCoffeeDto) { // <<<
return this.coffeesService.create(CreateCoffeeDto); // <<<
}
...
...
}
Let’s change the property name here from body
to CreateCoffeeDto
, and set it
to the Type of CreateCoffeeDto
as well.
One last thing here. Let’s make sure we change any instances of "body" in the
method to CreateCoffeeDto
and we’re all set.
We now have full Type safety within our method, letting us know exactly what to expect for a payload.
As we could see, DTO’s are just simple - Objects, they don’t contain any business logic, methods or anything that require testing. We are just trying to create the shape or Object - Interface of what our data transfer - object is.
One other great best practice with DTO’s is, marking all of the properties as readonly to help maintain immutability. So let’s go back and that to each one of our properties real quick.
// coffee.entity.ts
export class CreateCoffeeDto {
readonly id: number;
readonly name: string;
readonly brand: string;
readonly flavors: string[];
}
Adding the keyword readonly
right before the name of each properties.
Great, so you might have noticed so far, that our CreateCoffeeDto
is almost
identical to our coffee.entity
. This my seem redundant, for the time being.
But, that’s just because we don’t have an external data-source just yet. We’re
also dealing with a mock - Entity for the time being. We’ll see how different
the two of these are, when we dive into real entities later in this course.
To really help drive all of this home, let’s also create another DTO - class
for our PATCH - request. Let’s call it UpdateCoffeeDto
. Just like before,
we’ll generate the class via the Nest - CLI with,
$ nest g class coffees/dto/update-coffee.dto --no-spec
CREATE src/coffees/dto/update-coffee.dto.ts (32 bytes)
Remember, we generate update-coffee.dto
with --no-spec
flag.
Since UPDATE and CREATE operations typically require all of the same class fields with the same types. Let’s copy and paste al the properties from our Create - DTO so we can bring them over to our Update - DTO.
// update-coffee.dto.ts
export class UpdateCoffeeDto {
readonly id?: number;
readonly name?: string;
readonly brand?: string;
readonly flavors?: string[];
}
We’ll look at ways to avoid this type of repetitiveness in the next video as well.
One big difference that we need for our update - DTO is, that we want all the
properties here to be "optional". With TypeScript, you can easily achieve this
with the question-mark ?
. So let’s make sure we add the ?
before the
':'
(colon) for each property here.
Let’s pop back over to our CoffeesController
, head to the PATCH - request, and
let’s add this new Type for our method signature here.
// coffee.controller.ts
import { Controller, Get, Param, Body, Post, HttpCode, HttpStatus, Res, Patch, Delete, Query } from '@nestjs/common';
import { CoffeesService }from './coffees.service';
import { CreateCoffeeDto } from './dto/create-coffee.dto';
@Controller('coffees')
export class CoffeesController {
...
...
@Patch(':id')
update(@Param('id') id: string, @Body() body) { // <<<
return this.coffeesService.update(id, body); // <<<
}
...
...
}
Just like before, let’s replace 'body'
property with UpdateCoffeeDto
and
set the Type to updateCoffeeDto:
as well.
// coffee.controller.ts
import { Controller, Get, Param, Body, Post, HttpCode, HttpStatus, Res, Patch, Delete, Query } from '@nestjs/common';
import { CoffeesService }from './coffees.service';
import { CreateCoffeeDto } from './dto/create-coffee.dto';
import { UpdateCoffeeDto } from './dto/update-coffee.dto';
@Controller('coffees')
export class CoffeesController {
...
...
@Patch(':id')
update(@Param('id') id: string, @Body() updateCoffeeDto: UpdateCoffeeDto) { // <<<
return this.coffeesService.update(id, updateCoffeeDto); // <<<
}
...
...
}
Now, That we’re using UpdateCoffeeDto
, with this optional properties, we can
pass in any combination of properties for our payload, which is exactly what
we want with a PATCH - request.
As we learned of previous lesson, a PATCH - request can update any portion of a resource, no matter how small. With newly added DTO, we have the power of Type - safety and full flexibility here.
We’ve only begun to scratch the surface of the power of DTO’s. In the next section, let’s see how we can validate this input data, and so much more.
As we learned in the last lesson, Data Transfer Object or DTO’s are useful in creating a bit of Type - safety within our application. DTO’s let us create a definition for the shape of the data that’s coming into the body of an API requests.
But we don’t know who or what is calling these requests. How can we make sure the data that’s coming in, is in the correct shape? Or if it’s missing required fields?
It’s a common best practice for any back-end, to validate the correctness of data being sent into our application, and it’s even more ideal if we can automatically validate these incoming requests.
NestJS provides ValidationPipe[11] to solve this problems. The ValidationPipe provides a convenient way of enforcing validation rules for all incoming client - payloads.
You can specify these rules by using a simple annotation in your DTO. Before we start using it, Let’s setup our entire application to use the ValidationPipe
Let’s open our main.ts
file, We add the following line,
app.useGlobalPipes(new ValidationPipe());
Making sure, to import it from @nestjs/common
.
// main.ts
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestrjs/common';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe()); // <<<
await app.listen(3002);
}
bootstrap();
Note
|
that are many other ways of binding global Pipes that will dive into, in later chapters. |
Next, we’re going to have to install two packages in root project. Let’s open our terminal,
$ npm i --save class-validator class-transformer
We install class-validator[12] and class-transformer[13]. We’ll be continuing ahead, as we already have these installed. But if you’re following along, just pause in a second, and come back when it’s finished.
With everything installed, and our ValidationPipe in place, we can start adding validation - rules to our DTO now.
Let’s open up our CreateCoffeeDto
,
// create-coffee.dto
import { IsString } from 'class-validator';
export class CreateCoffeeDto {
@IsString()
readonly name: string;
@IsString()
readonly brand: string;
@IsString({each: true})
readonly flavors: string[];
}
Since most of the item here are String’s. Let’s start importing IsString
from
class-validator
. Now, let’s add a @IsString()
decorator before the name
and brand
properties, making them required. For flavors
let’s us
@IsString()
with an Object {each: }
set to true
. {each: true}
indicates
that expected value is an Array of String’s.
class-validator
has a lot of other great options, make sure to check out the
documentation[14] for lots of other great available decorators.
So, how did these decorator’s (@IsString()
) help us? Well, we can see that
with the help of class-validator
. We’ve given instruction for our DTO and
set up rules, for things like: 1
, What is required, and 2
, What is
type do we expect certain property to be.
Now that we have these validation - rules in places, if a request hits our
endpoint with an invalid - property in the request - body, the application
will automatically respond with a 400 - BadRequest
code, giving us automatic
feedback and a way to test our payloads effortlessly.
Let’s test how these validation - rules work by passing in a few different key - values and see how endpoint react.
First, let’s call the POST - Coffees endpoint, set our body to JSON and enter
in something like {"name": "Shipwreck Roast"}
// request 'POST - http://localhost:3002/coffees'
// Body - raw: JSON
{
"name": "Shipwreck Roast"
}
// response, 400 - Bad Request
{
"statusCode": 400,
"message": [
"brand must be a string",
"each value in flavors must be a string"
],
"error": "Bad Request"
}
It looks like it worked, our API responded with 400 Bad Request
, and it even
gave us a message Array, letting us know exactly what was wrong with *each one*
of our fields. Our API knew that brand
must be a String, and flavors
, is an
Array of String’s.
We didn’t pass in either one of these with our request. But we can see that it was expecting them in your payload.
Next let’s make another request, but this time let’s pass in two additional
properties. First, let’s pass in "brand":
and enter in a random String for the
value. Secondly, let’s pass in "flavors": [1, 2]
, but for the value let’s pass
in an Array of Numbers, and see how our API reacts this time.
// request 'POST - http://localhost:3002/coffees'
// Body - raw: JSON
{
"name": "Shipwreck Roast",
"brand": "Shipwreck brand",
"flavors": [1, 2]
}
// response, 400 - Bad Request
{
"statusCode": 400,
"message": [
"each value in flavors must be a string"
],
"error": "Bad Request"
}
Once again, the validation we set up works perfectly. We can see that each value of `"flavors"', does in fact need to be a String, and we got an error message letting us know just that.
Let’s remove those numbers, and add a random String "flavors"
like "caramel",
for example, hit send.
// request 'POST - http://localhost:3002/coffees'
// Body - raw: JSON
{
"name": "Shipwreck Roast",
"brand": "Shipwreck brand",
"flavors": ["caramel"]
}
// response, 201 - Created
No 'body' returned for 'response'
There you go, our validation finally passed and we can see we got a 201
- Created
response back.
All right, so now that we’ve seen how the basic of validation works, let’s
circle back to our UpdateCoffeeDto
.
// update.coffee.dto.ts
export class UpdateCoffeeDto {
readonly id?: number;
readonly name?: string;
readonly brand?: string;
readonly flavors?: string[];
}
If you remember when we created this DTO, we had to copy the values from our
CreateCoffeeDto
and change some of those properties. This is a bit a redundant
code - smell, isn’t it? Let’s see what we could have done better, and how Nest
can help simplify this very common task.
NestJS provides several utility functions as part of the package
@nestjs/mapped-types
[15]. These functions help us quickly
perform these types of common transformations.
Back to our terminal, let’s install the @nestjs/mapped-types
package in our
root directory and see how we can utilize in our code.
$ npm i --save @nestjs/mapped-types
We’ll continue ahead, as we already have this packages installed, but if you’re following along, just pause a second, and come back when it’s finished.
Once finished, let’s head over to our UpdateCoffeeDto
and see how we could
avoid all this redundant code.
// update.coffee.dto.ts
import { PartialType } from '@nestjs/mapped-types';
import { CreateCoffeeDto } from './create-coffee.dto';
export class UpdateCoffeeDto extends PartialType(CreateCoffeeDto) {}
First, let’s remove all of the code inside our UpdateCoffeeDto
, and extends
this class with helpers function called "PartialTypes()"
. Let’s make sure we
import PartialTypes
from @nestjs/mapped-types
.
PartialTypes
is expecting a Type to be passed inside of it. So let’s pass in
CreateCoffeeDto
.
This PartialTypes
function is really helpful, because what it’s doing for us
is, returning the Type of the class we passed into it, with all the properties
set to optional; And just like that no more duplicate code!.
PartialTypes
not only marks, all the fields is optional, but it also inherits
all the validation - rules via decorators, as well as adds a single additional
validation rule to each field the @IsOptional()
rule on the fly.
Let’s save our changes, wait for the compilation to finish, and head over to
insomnia
to test this out.
Since all properties are labeled as optional now, thanks to
@nestjs/mapped-types
package, let’s make a PATCH - request to /coffees/1
,
and remove everything BUT the "name" key-value in our request - payload.
// request 'PATCH - http://localhost:3002/coffees/1'
// Body - raw: JSON
{
"name": "Shipwreck Roast",
}
// response, 200 - OK
No 'body' returned for 'response'
Great. It worked just like what we wanted!. You can see we got 200 - OK
response back and no validation errors for the missing properties.
If we try too pass in something incorrect, such as Number or a Boolean
for "name":
value.
// request 'PATCH - http://localhost:3002/coffees/1'
// Body - raw: JSON
{
"name": true
}
// response, 400 - Bad Request
{
"statusCode": 400,
"message": [
"name must be a string"
],
"error": "Bad Request"
}
We’ll see that UpdateCoffeeDto
really did inherit all the validation rules as
we get a 400 - Bad Request
here as well.
The validationPipe
has many other great features. For example, it can filter
out properties that should NOT be received by a method - handler, via
"whitelisting". By "whitelisting" acceptable properties, any property NOT
included in the "whitelist" is automatically stripped from the resulting
- object.
Let’s open main.ts
an implement "whitelist",
// main.ts
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({
whitelist: true
}));
await app.listen(3002);
}
bootstrap();
We could enable this by simply entering some options to ValidationPipe
. In our
main.ts
file, Let’s pass in an object inside of ValidationPipe
with
key-value whitelist: true
inside of it.
So why is this helpful for us? Well, let’s say we want to avoid users passing in
invalid properties to our CoffeesController
POST - request when they’re
creating New - Coffees.
This "whitelist" feature will make sure all those unwanted or invalid
- properties are automatically stripped out and removed. With the power of
DTO’s and validationsPie
- "whitelist" feature all of this is now
possible to us effortlessly.
To see all of this in action, let’s open up CoffeesService
and make sure to
return the DTO itself - back to the client in our create()
- method.
import { Injectable, HttpException, HttpStatus, NotFoundException } from '@nestjs/common';
import { Coffee } from './entities/coffee.entity';
@Injectable()
export class CoffeesService {
...
...
create(createCoffeeDto: any) {
this.coffees.push(createCoffeeDto);
return createCoffeeDto; // <<<
}
...
...
}
Let’s save our changes, open in insomnia
, and send a POST - request to
/coffes
endpoint.
Let’s pass some properties from our DTO like "name":
, "brand":
and
"flavors":
, but let’s pas an additional key-value that ISN’T part of our
DTO!. In our case, we’re going to send something random like "isEnabled": true
// request 'POST - http://localhost:3002/coffees'
// Body - raw: JSON
{
"name": "Shipwreck Roast",
"brand": "Shipwreck brand",
"flavors": ["caramel"],
"isEnabled": true
}
// response, 400 - Bad Request
{
"name": "Salemba Brew",
"brand": "Salemba Brand",
"flavors": [
"caramel"
]
}
Once we hit send, and look at our response, we’ll see only the properties of our
DTO are echoed back to us. The rest were automatically removed by the
ValidationPipe
- "whitelist" feature we just set up.
In addition to this, the ValidationPipe
also gives us the option to STOP
a request from being processed if any "non-whitelisted" are present. Throwing
an error instead.
Let’s head back to our main.ts
file, and add the "forbidNonWhitelisted"
- option and set it to true
.
// main.ts
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true
}));
await app.listen(3002);
}
bootstrap();
This property, in combination with "whitelist", will be enable this functionality right away.
Saving our changes, Let’s open up the previous example with unwanted key-values, and hit send again,
// request 'POST - http://localhost:3002/coffees'
// Body - raw: JSON
{
"name": "Shipwreck Roast",
"brand": "Shipwreck brand",
"flavors": ["caramel"],
"isEnabled": true
}
// response, 400 - Bad Request
{
"statusCode": 400,
"message": [
"property isEnabled should not exist"
],
"error": "Bad Request"
}
We can see now, that the server - responded with an Error and even told us what properties caused this error to happen!.
When we receive request with payloads. These payloads typically come over the network as plain JavaScript - Objects. This is done by design, to help make everything is performing as possible. But how can we ensure, that the payloads come in the shape we expect them to be?
Let’s take look at our POST - request in our CoffeesController
and add
a console.log()
to see what Type our request - body CreateCoffeeDto
is,
and let’s also check whether it’s an "instanceof"
the CreateCoffeeDto"
- class.
// coffees.controller.ts
import { Controller, Get, Param, Body, Post, HttpCode, HttpStatus, Res, Patch, Delete, Query } from '@nestjs/common';
import { CoffeesService }from './coffees.service';
import { CreateCoffeeDto } from './dto/create-coffee.dto';
import { UpdateCoffeeDto } from './dto/update-coffee.dto';
@Controller('coffees')
export class CoffeesController {
constructor(private readonly coffeesService: CoffeesService) {}
...
...
@Post()
create(@Body() createCoffeeDto: CreateCoffeeDto) {
console.log("===>", createCoffeeDto instanceof CreateCoffeeDto);
return result = this.coffeesService.create(createCoffeeDto);
}
...
...
}
Now, let’s save our changes, open up insomnia
, and hit this POST - endpoint.
If we open up our terminals now, we’ll see "false" was logged.
[Nest] 91872 - 03/24/2021, 11:29:34 PM [NestApplication] Nest application successfully started +3ms
===> false
So, it turns out our payload may be in the "shape" of CreateCoffeeDto
, but
it’s not actually an instance of our CreateCoffeeDto
- class just yet.
Lucky for us, ValidationPipe
can help us transform this Object into exactly
what we’re expecting. To enable this behavior globally, let’s head over to our
main.ts
file,
// main.ts
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({
whitelist: true,
transform: true,
forbidNonWhitelisted: true
}));
await app.listen(3002);
}
bootstrap();
We set the "transform:" - options to true
, on our global ValidationPipe
.
Let’s save our changes and test the endpoint again.
[Nest] 91872 - 03/24/2021, 11:32:34 PM [NestApplication] Nest application successfully started +3ms
===> true
As we could see, the createCoffeeDto instanceof CreateCoffeeDto
- expression
showed "true" in our terminal now!.
So, what else can this "transform": - ValidationPipe
feture do? This auto
- transformation feature also performs primitive Type - conversions for things
such as Boolean and Numbers.
If we look at our findOne()
- GET method within CoffeesController
,
// coffees.controller.ts
import { Controller, Get, Param, Body, Post, HttpCode, HttpStatus, Res, Patch, Delete, Query } from '@nestjs/common';
import { CoffeesService }from './coffees.service';
import { CreateCoffeeDto } from './dto/create-coffee.dto';
import { UpdateCoffeeDto } from './dto/update-coffee.dto';
@Controller('coffees')
export class CoffeesController {
constructor(private readonly coffeesService: CoffeesService) {}
...
...
@Get(':id')
findOne(@Param('id') id: string) {
return this.coffeesService.findOne(id);
}
...
...
}
It takes one argument which represents and extracted 'id' - path parameter, that we know is of Type "Number". However, by default every path - parameter and query - parameter come over the network as a String.
// coffees.controller.ts
import { Controller, Get, Param, Body, Post, HttpCode, HttpStatus, Res, Patch, Delete, Query } from '@nestjs/common';
import { CoffeesService }from './coffees.service';
import { CreateCoffeeDto } from './dto/create-coffee.dto';
import { UpdateCoffeeDto } from './dto/update-coffee.dto';
@Controller('coffees')
export class CoffeesController {
constructor(private readonly coffeesService: CoffeesService) {}
...
...
@Get(':id')
findOne(@Param('id') id: number) {
return this.coffeesService.findOne(id);
}
...
...
}
If we changes the Type of 'id'
to "Number". ValidationPipe
will try to
automatically convert the String - identifier to a Number. Just like that.
Since our CoffeesService
- findOne()
expects String,
// coffees.service.ts
import { Injectable, HttpException, HttpStatus, NotFoundException } from '@nestjs/common';
import { Coffee } from './entities/coffee.entity';
@Injectable()
export class CoffeesService {
...
...
findOne(id: string) { // <<<
// throw "A Random Error";
const coffee = this.coffees.find(item => item.id === +id);
if (!coffee) {
// throw new HttpException(`Coffee #${id} not found`, HttpStatus.NOT_FOUND);
throw new NotFoundException(`Coffee #${id} not found`);
}
return coffee;
// return this.coffees.find(item => item.id === +id);
}
...
...
}
Let’s temporarily work around this to prevent compilation errors.
// coffees.controller.ts
import { Controller, Get, Param, Body, Post, HttpCode, HttpStatus, Res, Patch, Delete, Query } from '@nestjs/common';
import { CoffeesService }from './coffees.service';
import { CreateCoffeeDto } from './dto/create-coffee.dto';
import { UpdateCoffeeDto } from './dto/update-coffee.dto';
@Controller('coffees')
export class CoffeesController {
constructor(private readonly coffeesService: CoffeesService) {}
...
...
@Get(':id')
findOne(@Param('id') id: number) {
console.log("GET ===>" typeof id)
return this.coffeesService.findOne('' + id); // <<<
}
...
...
}
To test whether this auto transformation feature works, let’s add a single
console.log()
inside of this findOne()
method to test what Type - "id" is
now.
Now, if we call this endpoint using insomnia
. Let’s call GET - request on
/coffees/1
// request 'GET - http://localhost:3002/coffees/1'
// Body - raw: JSON
{}
// response, 200 - OK
{
"id": 1,
"name": "Salemba Roast",
"brand": "Salemba Brand",
"flavors": [
"chocolate",
"vanilla"
]
}
We’ll see the typeof id
arguments is, in fact, "number"!.
[Nest] 129771 - 03/24/2021, 11:59:50 PM [NestApplication] Nest application successfully started +3ms
GET ===> number
As we can see, this feature of validation is incredibly helpful. It not only saves us time, but also helps us be more aware of what Types we’re dealing with, whether they are primitive, like Boilean, Number or even our custom - DTO’s.
Caution
|
Just be aware that this feature may very slightly impact performance. |
So, just make sure it works great for your application, and if speed is essential, test you endpoints to make sure that performance difference is negligible[16]