Hello, Habr! I present to you the translation of the article "Full-Stack TypeScript Apps - Part 1: Developing Backend APIs with Nest.js" by Ana Ribeiro .
Part 1: Developing the server API using Nest.JS
TL; DR: This is a series of articles on how to create a TypeScript web application using Angular and Nest.JS. In the first part, we will write a simple server API using Nest.JS. The second part of this series is devoted to the front-end application using Angular. You can find the final code developed in this article in this GitHub repository.
What is Nest.Js and why Angular?
Nest.js is a framework for building Node.js web server applications.
A distinctive feature is that it solves a problem that no other framework solves: the structure of the node.js. project If you have ever developed under node.js, you know that you can do a lot with one module (for example, Express middleware can do everything from authentication to validation), which, in the end, can lead to an unsupported "mess" . As you will see below, nest.js will help us with this by providing classes that specialize in various problems.
Nest.js is heavily inspired by Angular. For example,
Web application for online orders
In this guide, we will create a simple application in which users can place orders in a restaurant. It will implement this logic:
- any user can view the menu;
- Only an authorized user can add goods to the basket (make an order)
- only the administrator can add new menu items.
For simplicity, we will not interact with an external database and do not implement the functionality of our store basket.
Creating the file structure of the Nest.js project
To install Nest.js, we need to install Node.js (v.8.9.x or higher) and NPM. Download and install Node.js for your operating system from the official website (NPM is included). When everything is installed, check the versions:
node -v # v12.11.1 npm -v # 6.11.3
There are different ways to create a project with Nest.js; they can be found in the documentation . We will use nest-cli
. Install it:
npm i -g @nestjs/cli
Next, create our project with a simple command:
nest new nest-restaurant-api
in the process, nest will ask us to choose a package manager: npm
or yarn
If everything went well, nest
will create the following file structure:
nest-restaurant-api โโโ src โ โโโ app.controller.spec.ts โ โโโ app.controller.ts โ โโโ app.module.ts โ โโโ app.service.ts โ โโโ main.ts โโโ test โ โโโ app.e2e-spec.ts โ โโโ jest-e2e.json โโโ .gitignore โโโ .prettierrc โโโ nest-cli.json โโโ package.json โโโ package-lock.json โโโ README.md โโโ tsconfig.build.json โโโ tsconfig.json โโโ tslint.json
go to the created directory and start the development server:
# cd nest-restaurant-api # npm run start:dev
Open a browser and enter http://localhost:3000
. On the screen we will see:
As part of this guide, we will not be testing our API (although you should write tests for any ready-to-use application). This way you can clear the test
directory and delete the src/app.controller.spec.ts
(which is the test one). As a result, our source folder contains the following files:
-
src/app.controller.ts
andsrc/app.module.ts
: these files are responsible for creating theHello world
message along the/
route. Because this entry point is not important for this application we delete them. Soon you will learn in more detail what controllers and services are . -
src/app.module.ts
: contains a description of a class of type module , which is responsible for declaring the import, export of controllers and providers to the nest.js application. Each application has at least one module, but you can create more than one module for more complex applications (more in the documentation . Our application will contain only one module -
src/main.ts
: this is the file responsible for starting the server.
Note: after removing src/app.controller.ts
andsrc/app.module.ts
you will not be able to start our application. Donโt worry, we will fix it soon.
Creating entry points (endpoints)
Our API will be available on the /items
route. Through this entry point, users can receive data, and administrators manage the menu. Let's create it.
To do this, create a directory called items
inside src
. All files associated with the /items
route will be stored in this new directory.
Creating controllers
in nest.js
, as in many other frameworks, controllers are responsible for mapping routes with functionality. To create a controller in nest.js
use the nest.js
decorator as follows: @Controller(${ENDPOINT})
. Further, in order to map various HTTP
methods, such as GET
and POST
, decorators @Get
, @Post
, @Delete
, etc. are used.
In our case, we need to create a controller that returns dishes available in the restaurant, and which administrators will use to manage the contents of the menu. Let's create a file called items.controller.tc
in the src/items
directory with the following contents:
import { Get, Post, Controller } from '@nestjs/common'; @Controller('items') export class ItemsController { @Get() async findAll(): Promise<string[]> { return ['Pizza', 'Coke']; } @Post() async create() { return 'Not yet implemented'; } }
in order to make our new controller available in our application, register it in the module:
import { Module } from '@nestjs/common'; import { ItemsController } from './items/items.controller'; @Module({ imports: [], controllers: [ItemsController], providers: [], }) export class AppModule {}
Launch our application: npm run start:dev
and open in the browser http: // localhost: 3000 / items , if you did everything correctly, then we should see the answer to our get request: ['Pizza', 'Coke']
.
Translator's note: to create new controllers, as well as other elements of nest.js
: services, providers, etc., it is more convenient to use the nest generate
command from the nest-cli
. For example, to create the controller described above, you can use the nest generate controller items
command, as a result of which nest will create src/items/items.controller.spec.tc
and src/items/items.controller.tc
following contents:
import { Get, Post, Controller } from '@nestjs/common'; @Controller('items') export class ItemsController {}
and register it in app.molule.tc
Adding a Service
Now, when accessing /items
our application returns the same array for each request, which we cannot change. Processing and saving data is not the controller's business; for this purpose services are intended in nest.js
Services in nest are @Injectable
The name of the decorator speaks for itself, adding this decorator to the class makes it injectable into other components, such as controllers.
Let's create our service. Create the items.service.ts
file in the items.service.ts
folder with the following contents:
import { Injectable } from '@nestjs/common'; @Injectable() export class ItemsService { private readonly items: string[] = ['Pizza', 'Coke']; findAll(): string[] { return this.items; } create(item: string) { this.items.push(item); } }
and change the ItemsController
controller (declared in items.controller.ts
) so that it uses our service:
import { Get, Post, Body, Controller } from '@nestjs/common'; import { ItemsService } from './items.service'; @Controller('items') export class ItemsController { constructor(private readonly itemsService: ItemsService) {} @Get() async findAll(): Promise<string[]> { return this.itemsService.findAll(); } @Post() async create(@Body() item: string) { this.itemsService.create(item); } }
in the new version of the controller, we applied the @Body
decorator to the create
method argument. This argument is used to automatically match the data passed through req.body ['item']
to the argument itself (in this case, item
).
Our controller also receives an instance of the ItemsService
class, injected via the constructor. Declaring ItemsService
as private readonly
makes an instance immutable and visible only inside the class.
And do not forget to register our service in app.module.ts
:
import { Module } from '@nestjs/common'; import { ItemsController } from './items/items.controller'; import { ItemsService } from './items/items.service'; @Module({ imports: [], controllers: [ItemsController], providers: [ItemsService], }) export class AppModule {}
After all the changes, let's send an HTTP POST request to the menu:
curl -X POST -H 'content-type: application/json' -d '{"item": "Salad"}' localhost:3000/items
Then weโll check whether new dishes appeared on our menu by making a GET request (or by opening http: // localhost: 3000 / items in a browser)
curl localhost:3000/items
Creating a Shopping Cart Route
Now that we have the first version of the entry points /items
our API, let's implement the shopping cart functionality. The process of creating this functionality differs little from the already created API. Therefore, in order not to clutter up the manual, we will create a component that responds with OK status when accessing.
First, in the ./src/shopping-cart/
folder ./src/shopping-cart/
create the shoping-cart.controller.ts
:
import { Post, Controller } from '@nestjs/common'; @Controller('shopping-cart') export class ShoppingCartController { @Post() async addItem() { return 'This is a fake service :D'; } }
Register this controller in our module ( app.module.ts
):
import { Module } from '@nestjs/common'; import { ItemsController } from './items/items.controller'; import { ShoppingCartController } from './shopping-cart/shopping-cart.controller'; import { ItemsService } from './items/items.service'; @Module({ imports: [], controllers: [ItemsController, ShoppingCartController], providers: [ItemsService], }) export class AppModule {}
To verify this entry point, run the following command, after making sure that the application is running:
curl -X POST localhost:3000/shopping-cart
Adding an Interface Typescript for Items
Back to our items
service. Now we save only the name of the dish, but this is clearly not enough, and, for sure, we will want to have more information (for example, the cost of the dish). I think you will agree that storing this data as an array of strings is not a good idea?
To solve this problem, we can create an array of objects. But how to maintain the structure of objects? Here the TypeScript interface will help us, in which we define the structure of the items
object. Create a new file named item.interface.ts
in the src/items
folder:
export interface Items { readonly name: string; readonly price: number; }
Then items.service.ts
file:
import { Injectable } from '@nestjs/common'; import { Item } from './item.interface'; @Injectable() export class ItemsService { private readonly items: Item[] = []; findAll(): Item[] { return this.items; } create(item: Item) { this.items.push(item); } }
And also in items.controller.ts
:
import { Get, Post, Body, Controller } from '@nestjs/common'; import { ItemsService } from './items.service'; import { Item } from './item.interface'; @Controller('items') export class ItemsController { constructor(private readonly itemsService: ItemsService) {} @Get() async findAll(): Promise<Item[]> { return this.itemsService.findAll(); } @Post() async create(@Body() item: Item) { this.itemsService.create(item); } }
Validation of input in Nest.js
Despite the fact that we determined the structure of the item
object, our application will not return an error if we send an invalid POST request (any type of data not defined in the interface). For example, for such a request:
curl -H 'Content-Type: application/json' -d '{ "name": 3, "price": "any" }' http://localhost:3000/items
the server should respond with a status of 400 (bad request), but instead, our application will respond with a status of 200 (OK).
To solve this problem, create a DTO (Data Transfer Object) and a Pipe component (channel).
DTO is an object that defines how data should be transferred between processes. We describe the DTO in the src/items/create-item.dto.ts
:
import { IsString, IsInt } from 'class-validator'; export class CreateItemDto { @IsString() readonly name: string; @IsInt() readonly price: number; }
Pipes in Nest.js
are the components used for validation. For our API, create a channel in which it checks whether the data sent to the method matches the DTO. One channel can be used by different controllers, so create the src/common/
directory with the file validation.pipe.ts
:
import { ArgumentMetadata, BadRequestException, Injectable, PipeTransform, } from '@nestjs/common'; import { validate } from 'class-validator'; import { plainToClass } from 'class-transformer'; @Injectable() export class ValidationPipe implements PipeTransform<any> { async transform(value, metadata: ArgumentMetadata) { const { metatype } = metadata; if (!metatype || !this.toValidate(metatype)) { return value; } const object = plainToClass(metatype, value); const errors = await validate(object); if (errors.length > 0) { throw new BadRequestException('Validation failed'); } return value; } private toValidate(metatype): boolean { const types = [String, Boolean, Number, Array, Object]; return !types.find(type => metatype === type); } }
Note: We need to install two modules:class-validator
andclass-transformer
. To do this, runnpm install class-validator class-transformer
in the console and restart the server.
Adapting items.controller.ts
for use with our new pipe and DTO:
import { Get, Post, Body, Controller, UsePipes } from '@nestjs/common'; import { CreateItemDto } from './create-item.dto'; import { ItemsService } from './items.service'; import { Item } from './item.interface'; import { ValidationPipe } from '../common/validation.pipe'; @Controller('items') export class ItemsController { constructor(private readonly itemsService: ItemsService) {} @Get() async findAll(): Promise<Item[]> { return this.itemsService.findAll(); } @Post() @UsePipes(new ValidationPipe()) async create(@Body() createItemDto: CreateItemDto) { this.itemsService.create(createItemDto); } }
Let's check our code again, now the entry /items
accepts data only if they are defined in the DTO. For example:
curl -H 'Content-Type: application/json' -d '{ "name": "Salad", "price": 3 }' http://localhost:3000/items
Paste in invalid data (data that cannot be verified in ValidationPipe
), as a result we get the answer:
{"statusCode":400,"error":"Bad Request","message":"Validation failed"}
User Authentication with Auth0
during...