Full-stack TypeScript Apps

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, both platforms use guards to allow or prevent access to some parts of your applications, and both platforms provide a CanActivate interface for implementing these guards. However, it is important to note that, despite some similar concepts, both structures are independent of each other. That is, in this article, we will create an independent API for our front-end, which can be used with any other framework (React, Vue.JS and so on).







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:









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:









Note: after removing src/app.controller.ts



and src/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



and class-transformer



. To do this, run npm 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...



All Articles