Hola Habr! Le presento la traducción del artículo "Aplicaciones TypeScript Full-Stack - Parte 1: Desarrollo de API de backend con Nest.js" de Ana Ribeiro .
Parte 1: Desarrollar la API del servidor usando Nest.JS
TL; DR: Esta es una serie de artículos sobre cómo crear una aplicación web TypeScript usando Angular y Nest.JS. En la primera parte, escribiremos una API de servidor simple usando Nest.JS. La segunda parte de esta serie está dedicada a la aplicación front-end usando Angular. Puede encontrar el código final desarrollado en este artículo en este repositorio de GitHub.
¿Qué es Nest.Js y por qué Angular?
Nest.js es un marco para crear aplicaciones de servidor web Node.js.
Una característica distintiva es que resuelve un problema que ningún otro marco resuelve: la estructura del proyecto node.js. Si alguna vez ha desarrollado bajo node.js, sabe que puede hacer mucho con un módulo (por ejemplo, el middleware Express puede hacer todo, desde la autenticación hasta la validación), lo que finalmente puede conducir a un "desastre" no admitido . Como verá a continuación, nest.js nos ayudará con esto al proporcionar clases que se especializan en varios temas.
Nest.js está fuertemente inspirado en Angular. Por ejemplo ambas plataformas usan guardias para permitir o impedir el acceso a algunas partes de sus aplicaciones, y ambas plataformas proporcionan una interfaz CanActivate para implementar estos guardias. Sin embargo, es importante tener en cuenta que, a pesar de algunos conceptos similares, ambas estructuras son independientes entre sí. Es decir, en este artículo, crearemos una API independiente para nuestro front-end, que se puede usar con cualquier otro marco (React, Vue.JS, etc.).
Aplicación web para pedidos en línea
En esta guía, crearemos una aplicación simple en la que los usuarios pueden realizar pedidos en un restaurante. Implementará esta lógica:
- cualquier usuario puede ver el menú;
- solo un usuario autorizado puede agregar productos a la cesta (hacer un pedido)
- solo el administrador puede agregar nuevos elementos de menú.
Para simplificar, no interactuaremos con una base de datos externa y no implementaremos la funcionalidad de nuestra cesta de la tienda.
Crear la estructura de archivos del proyecto Nest.js
Para instalar Nest.js, necesitamos instalar Node.js (v.8.9.xo superior) y NPM. Descargue e instale Node.js para su sistema operativo desde el sitio web oficial (NPM está incluido). Cuando todo esté instalado, verifique las versiones:
node -v
Hay diferentes formas de crear un proyecto con Nest.js; se pueden encontrar en la documentación . Usaremos nest-cli
. Instalarlo:
npm i -g @nestjs/cli
A continuación, cree nuestro proyecto con un comando simple:
nest new nest-restaurant-api
En el proceso, npm
nos pedirá que elijamos un administrador de paquetes: npm
o yarn
Si todo salió bien, nest
creará la siguiente estructura de archivos:
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
vaya al directorio creado e inicie el servidor de desarrollo:
Abra un navegador e ingrese http://localhost:3000
. En la pantalla veremos:

Como parte de este tutorial, no probaremos nuestra API (aunque debe escribir pruebas para cualquier aplicación lista para usar). De esta manera, puede borrar el directorio de test
y eliminar el src/app.controller.spec.ts
(que es el de prueba). Como resultado, nuestra carpeta de origen contiene los siguientes archivos:
src/app.controller.ts
y src/app.module.ts
: estos archivos son responsables de crear el mensaje Hello world
largo de la ruta /
. Porque este punto de entrada no es importante para esta aplicación, los eliminamos. Pronto aprenderá con más detalle qué son los controladores y los servicios .src/app.module.ts
: contiene una descripción de una clase de módulo de tipo, que es responsable de declarar la importación, exportación de controladores y proveedores a la aplicación nest.js. Cada aplicación tiene al menos un módulo, pero puede crear más de un módulo para aplicaciones más complejas (más en la documentación . Nuestra aplicación contendrá solo un módulosrc/main.ts
: este es el archivo responsable de iniciar el servidor.
Nota: después de eliminar src/app.controller.ts
y src/app.module.ts
no podrá iniciar nuestra aplicación. No se preocupe, lo arreglaremos pronto.
Crear puntos de entrada (puntos finales)
Nuestra API estará disponible en la ruta /items
. A través de este punto de entrada, los usuarios pueden recibir datos y los administradores administran el menú. Vamos a crearlo.
Para hacer esto, cree un directorio llamado items
dentro de src
. Todos los archivos asociados con la ruta /items
se almacenarán en este nuevo directorio.
Crear controladores
En nest.js
, como en muchos otros frameworks, los controladores son responsables de mapear rutas con funcionalidad. Para crear un controlador en nest.js
use el decorador nest.js
siguiente manera: @Controller(${ENDPOINT})
. Además, para mapear varios métodos HTTP
, como GET
y POST
, se @Get
los decoradores @Get
, @Post
, @Delete
, etc.
En nuestro caso, necesitamos crear un controlador que devuelva los platos disponibles en el restaurante y que los administradores utilizarán para administrar el contenido del menú. items.controller.tc
un archivo llamado items.controller.tc
en el directorio src/items
con el siguiente contenido:
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'; } }
Para que nuestro nuevo controlador esté disponible en nuestra aplicación, regístrelo en el módulo:
import { Module } from '@nestjs/common'; import { ItemsController } from './items/items.controller'; @Module({ imports: [], controllers: [ItemsController], providers: [], }) export class AppModule {}
Inicie nuestra aplicación: npm run start:dev
y abra en el navegador http: // localhost: 3000 / items , si hizo todo correctamente, entonces deberíamos ver la respuesta a nuestra solicitud de obtención: ['Pizza', 'Coke']
.
Nota del traductor: para crear nuevos controladores, así como otros elementos de nest.js
: servicios, proveedores, etc., es más conveniente usar el comando nest generate
del nest-cli
. Por ejemplo, para crear el controlador descrito anteriormente, puede usar el comando nest generate controller items
, como resultado de lo cual nest creará los src/items/items.controller.tc
src/items/items.controller.spec.tc
y src/items/items.controller.tc
siguientes contenidos:
import { Get, Post, Controller } from '@nestjs/common'; @Controller('items') export class ItemsController {}
y regístralo en app.molule.tc
Agregar un servicio
Ahora, al acceder a /items
nuestra aplicación devuelve la misma matriz para cada solicitud, que no podemos cambiar. Procesar y guardar datos no es asunto del controlador; para este propósito, los servicios están destinados en nest.js
Los servicios en el nido son @Injectable
El nombre del decorador habla por sí mismo, agregar este decorador a la clase lo hace inyectable en otros componentes, como los controladores.
Creemos nuestro servicio. Cree el archivo items.service.ts
en la carpeta items.service.ts
con el siguiente contenido:
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); } }
y cambie el controlador ItemsController
(declarado en items.controller.ts
) para usar nuestro servicio:
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); } }
En la nueva versión del controlador, aplicamos el decorador @Body
al argumento del método de create
. Este argumento se usa para hacer coincidir automáticamente los datos pasados a través de req.body ['item']
con el argumento en sí (en este caso, item
).
Además, nuestro controlador recibe una instancia de la clase ItemsService
, inyectada a través del constructor. Declarar ItemsService
como private readonly
hace que una instancia sea inmutable y visible solo dentro de la clase.
Y no olvide registrar nuestro servicio en 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 {}
Después de todos los cambios, enviemos una solicitud HTTP POST al menú:
curl -X POST -H 'content-type: application/json' -d '{"item": "Salad"}' localhost:3000/items
Luego verificaremos si aparecieron nuevos platos en nuestro menú haciendo una solicitud GET (o abriendo http: // localhost: 3000 / items en un navegador)
curl localhost:3000/items
Crear una ruta de carrito de compras
Ahora que tenemos la primera versión del punto de entrada /items
nuestra API, implementemos la funcionalidad del carrito de compras. El proceso de creación de esta funcionalidad no es muy diferente de la API ya creada. Por lo tanto, para no saturar el manual, crearemos un componente que responda con el estado OK al acceder.
Primero, en la carpeta ./src/shopping-cart/
cree el 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'; } }
Registre este controlador en nuestro módulo ( 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 {}
Para verificar este punto de entrada, ejecute el siguiente comando, después de asegurarse de que la aplicación se esté ejecutando:
curl -X POST localhost:3000/shopping-cart
Agregar un mecanografiado de interfaz para elementos
Volver a nuestro servicio de items
. Ahora guardamos solo el nombre del plato, pero esto claramente no es suficiente, y, por supuesto, queremos tener más información (por ejemplo, el costo del plato). ¿Creo que estará de acuerdo en que almacenar estos datos como una matriz de cadenas no es una buena idea?
Para resolver este problema, podemos crear una matriz de objetos. ¿Pero cómo salvar la estructura de los objetos? Aquí la interfaz TypeScript nos ayudará, en la que definimos la estructura del objeto de items
. Cree un nuevo archivo llamado item.interface.ts
en la carpeta src/items
:
export interface Items { readonly name: string; readonly price: number; }
Luego items.service.ts
archivo items.service.ts
:
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); } }
Y también en 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); } }
Validación de entrada en Nest.js
A pesar de que determinamos la estructura del item
del item
, nuestra aplicación no devolverá un error si enviamos una solicitud POST no válida (cualquier tipo de datos no definidos en la interfaz). Por ejemplo, para tal solicitud:
curl -H 'Content-Type: application/json' -d '{ "name": 3, "price": "any" }' http://localhost:3000/items
el servidor debe responder con un estado de 400 (solicitud incorrecta), pero nuestra aplicación responderá con un estado de 200 (OK).
Para resolver este problema, cree un DTO (objeto de transferencia de datos) y un componente de tubería (canal).
DTO es un objeto que define cómo se deben transferir los datos entre procesos. Describimos el DTO en el src/items/create-item.dto.ts
:
import { IsString, IsInt } from 'class-validator'; export class CreateItemDto { @IsString() readonly name: string; @IsInt() readonly price: number; }
Las tuberías en Nest.js
son los componentes utilizados para la validación. Para nuestra API, cree un canal en el que verifique si los datos enviados al método coinciden con el DTO. Los diferentes controladores pueden usar un canal, así que cree el directorio src/common/
con el archivo 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); } }
Nota: Necesitamos instalar dos módulos: class-validator
class-transformer
. Para hacer esto, ejecute npm install class-validator class-transformer
en la consola y reinicie el servidor.
Adaptando items.controller.ts
para usar con nuestro nuevo tubo y 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); } }
Revisemos nuestro código nuevamente, ahora la entrada /items
acepta datos solo si están definidos en el DTO. Por ejemplo:
curl -H 'Content-Type: application/json' -d '{ "name": "Salad", "price": 3 }' http://localhost:3000/items
Pegue datos no válidos (datos que no se pueden verificar en ValidationPipe
), como resultado obtenemos la respuesta:
{"statusCode":400,"error":"Bad Request","message":"Validation failed"}
Creando Middleware
Según la página de guía de inicio rápido de Auth0 , la forma recomendada de verificar el token JWT emitido por Auth0 es utilizar el middleware Express proporcionado por express-jwt
. Este middleware automatiza una gran parte del trabajo.
Creemos un archivo authentication.middleware.ts
dentro del directorio src / common
con el siguiente código:
import { NestMiddleware } from '@nestjs/common'; import * as jwt from 'express-jwt'; import { expressJwtSecret } from 'jwks-rsa'; export class AuthenticationMiddleware implements NestMiddleware { use(req, res, next) { jwt({ secret: expressJwtSecret({ cache: true, rateLimit: true, jwksRequestsPerMinute: 5, jwksUri: 'https://${DOMAIN}/.well-known/jwks.json', }), audience: 'http://localhost:3000', issuer: 'https://${DOMAIN}/', algorithm: 'RS256', })(req, res, err => { if (err) { const status = err.status || 500; const message = err.message || 'Sorry, we were unable to process your request.'; return res.status(status).send({ message, }); } next(); }); }; }
Reemplace ${DOMAIN}
con el valor de dominio de la configuración de la aplicación Auth0
Nota del traductor: en una aplicación real, saque DOMAIN
en una constante y establezca su valor a través de env
(entorno virtual)
Instale las jwks-rsa
express-jwt
y jwks-rsa
:
npm install express-jwt jwks-rsa
Es necesario conectar el middleware creado (controlador) a nuestra aplicación. Para hacer esto, en el archivo ./src/app.module.ts
:
import { Module, MiddlewareConsumer, RequestMethod } from '@nestjs/common'; import { AuthenticationMiddleware } from './common/authentication.middleware'; 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 { public configure(consumer: MiddlewareConsumer) { consumer .apply(AuthenticationMiddleware) .forRoutes( { path: '/items', method: RequestMethod.POST }, { path: '/shopping-cart', method: RequestMethod.POST }, ); } }
El código anterior dice que las solicitudes POST a las rutas /items
y /shopping-cart
están protegidas por el middleware Express , que verifica el token de acceso en la solicitud.
Reinicie el servidor de desarrollo ( npm run start:dev
) y llame a la API Nest.js:
Gestión de roles con Auth0
Por el momento, cualquier usuario con un token verificado puede publicar un elemento en nuestra API. Sin embargo, nos gustaría que solo los usuarios con derechos de administrador puedan hacer esto. Para implementar esta función, usamos las reglas (reglas) Auth0 .
Entonces, vaya al panel de control Auth0, en la sección Reglas . Allí, haga clic en el botón + CREATE RULE
y seleccione "Establecer roles para un usuario" como modelo de regla.

Una vez hecho esto, obtenemos un archivo JavaScript con una plantilla de regla que agrega la función de administrador a cualquier usuario que tenga un correo electrónico que pertenezca a un determinado dominio. Cambiemos algunos detalles en esta plantilla para obtener un ejemplo funcional. Para nuestra aplicación, solo le daremos al administrador acceso a nuestra propia dirección de correo electrónico. También tendremos que cambiar la ubicación para almacenar la información del estado del administrador.
Por el momento, esta información se almacena en un token de identificación (se usa para proporcionar información sobre el usuario), pero se debe usar un token de acceso para acceder a los recursos en la API. El código después de los cambios debería verse así:
function (user, context, callback) { user.app_metadata = user.app_metadata || {}; if (user.email && user.email === '${YOUR_EMAIL}') { user.app_metadata.roles = ['admin']; } else { user.app_metadata.roles = ['user']; } auth0.users .updateAppMetadata(user.user_id, user.app_metadata) .then(function() { context.accessToken['http://localhost:3000/roles'] = user.app_metadata.roles; callback(null, user, context); }) .catch(function(err) { callback(err); }); }
Nota: reemplace ${YOUR_EMAIL}
con su dirección de correo electrónico. Es importante tener en cuenta que, por regla general, cuando maneja el correo electrónico en las reglas Auth0, es ideal forzar la verificación del correo electrónico . En este caso, esto no es obligatorio porque utilizamos nuestra propia dirección de correo electrónico.
Nota del traductor: el fragmento de código anterior se ingresa en el navegador en la página de configuración de la regla Auth0
Para verificar si el token pasado a nuestra API es el token de administrador, necesitamos crear un guardia Nest.js. En la carpeta src/common
, cree el archivo admin.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; @Injectable() export class AdminGuard implements CanActivate { canActivate(context: ExecutionContext): boolean { const user = context.getArgs()[0].user['http://localhost:3000/roles'] || ''; return user.indexOf('admin') > -1; } }
Ahora, si repetimos el proceso de inicio de sesión descrito anteriormente y usamos la dirección de correo electrónico definida en la regla, obtendremos un nuevo access_token
. Para verificar el contenido de este access_token
, copie y pegue el token en el campo Encoded
del sitio https://jwt.io/
. Veremos que la sección de carga útil de este token contiene la siguiente matriz:
"http://localhost:3000/roles": [ "admin" ]
Si nuestro token realmente incluye esta información, continuamos la integración con Auth0. Entonces, abra items.controller.ts
y agregue nuestro nuevo protector allí:
import { Get, Post, Body, Controller, UsePipes, UseGuards, } 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'; import { AdminGuard } from '../common/admin.guard'; @Controller('items') export class ItemsController { constructor(private readonly itemsService: ItemsService) {} @Get() async findAll(): Promise<Item[]> { return this.itemsService.findAll(); } @Post() @UseGuards(new AdminGuard()) @UsePipes(new ValidationPipe()) async create(@Body() createItemDto: CreateItemDto) { this.itemsService.create(createItemDto); } }
Ahora, con nuestro nuevo token, podemos agregar nuevos elementos a través de nuestra API:
Nota del traductor: para la verificación, puede ver lo que tenemos en los artículos:
curl -X GET http://localhost:3000/items
Resumen
Felicidades ¡Acabamos de terminar de construir nuestra API Nest.JS y ahora podemos centrarnos en desarrollar la parte frontend de nuestra aplicación! Asegúrese de revisar la segunda parte de esta serie: Aplicaciones TypeScript Full-Stack - Parte 2: Desarrollo de aplicaciones angulares frontend.
Nota del traductor: la traducción de la segunda parte del proceso
Para resumir, en este artículo utilizamos varias características de Nest.js y TypeScript: módulos, controladores, servicios, interfaces, tuberías, middleware y protección para crear API Espero que tenga una buena experiencia y esté listo para continuar desarrollando nuestra aplicación. Si algo no está claro para usted, entonces la documentación oficial de nest.js es una buena fuente con respuestas