Olá Habr! Apresento a você a tradução do artigo "Aplicativos TypeScript de pilha completa - Parte 1: Desenvolvendo APIs de back-end com Nest.js", de Ana Ribeiro .
Parte 1: Desenvolvendo a API do servidor usando o Nest.JS
TL; DR: esta é uma série de artigos sobre como criar um aplicativo Web TypeScript usando Angular e Nest.JS. Na primeira parte, escreveremos uma API de servidor simples usando o Nest.JS. A segunda parte desta série é dedicada ao aplicativo front-end usando Angular. Você pode encontrar o código final desenvolvido neste artigo neste repositório GitHub.
O que é o Nest.Js e por que o Angular?
Nest.js é uma estrutura para a criação de aplicativos de servidor da web Node.js.
Uma característica distintiva é que ele resolve um problema que nenhuma outra estrutura resolve: a estrutura do projeto node.js. Se você já desenvolveu o node.js, sabe que pode fazer muito com um módulo (por exemplo, o middleware Express pode fazer de tudo, da autenticação à validação), o que, no final, pode levar a uma "bagunça" não suportada . Como você verá abaixo, o nest.js nos ajudará com isso, fornecendo aulas especializadas em vários problemas.
O Nest.js é fortemente inspirado pelo Angular. Por exemplo ambas as plataformas usam proteções para permitir ou impedir o acesso a algumas partes de seus aplicativos e ambas fornecem uma interface CanActivate para implementar essas proteções. No entanto, é importante notar que, apesar de alguns conceitos semelhantes, ambas as estruturas são independentes uma da outra. Ou seja, neste artigo, criaremos uma API independente para nosso front-end, que pode ser usada com qualquer outra estrutura (React, Vue.JS e assim por diante).
Aplicativo da Web para pedidos on-line
Neste guia, criaremos um aplicativo simples no qual os usuários podem fazer pedidos em um restaurante. Ele implementará esta lógica:
- qualquer usuário pode visualizar o menu;
- somente um usuário autorizado pode adicionar mercadorias à cesta (fazer um pedido)
- somente o administrador pode adicionar novos itens de menu.
Para simplificar, não interagiremos com um banco de dados externo e não implementaremos a funcionalidade do nosso carrinho de compras.
Criando a estrutura de arquivos do projeto Nest.js
Para instalar o Nest.js, precisamos instalar o Node.js (v.8.9.x ou superior) e o NPM. Baixe e instale o Node.js para o seu sistema operacional no site oficial (NPM está incluído). Quando tudo estiver instalado, verifique as versões:
node -v
Existem diferentes maneiras de criar um projeto com o Nest.js. eles podem ser encontrados na documentação . Vamos usar o nest-cli
. Instale-o:
npm i -g @nestjs/cli
Em seguida, crie nosso projeto com um comando simples:
nest new nest-restaurant-api
no processo, o nest solicitará que escolha um gerenciador de pacotes: npm
ou yarn
Se tudo der certo, o nest
criará a seguinte estrutura de arquivo:
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
vá para o diretório criado e inicie o servidor de desenvolvimento:
Abra um navegador e digite http://localhost:3000
. Na tela, veremos:

Como parte deste tutorial, não testaremos nossa API (embora você deva escrever testes para qualquer aplicativo pronto para uso). Dessa forma, você pode limpar o diretório de test
e excluir o src/app.controller.spec.ts
(que é o teste). Como resultado, nossa pasta de origem contém os seguintes arquivos:
src/app.controller.ts
e src/app.module.ts
: esses arquivos são responsáveis por criar a mensagem Hello world
ao longo da rota /
. Porque este ponto de entrada não é importante para esta aplicação, nós os excluímos. Em breve, você aprenderá com mais detalhes o que são controladores e serviços .src/app.module.ts
: contém uma descrição de uma classe do tipo módulo , responsável por declarar a importação, exportação de controladores e provedores para o aplicativo nest.js. Cada aplicativo possui pelo menos um módulo, mas você pode criar mais de um módulo para aplicativos mais complexos (mais na documentação . Nosso aplicativo conterá apenas um módulosrc/main.ts
: este é o arquivo responsável por iniciar o servidor.
Nota: após remover src/app.controller.ts
e src/app.module.ts
você não poderá iniciar nosso aplicativo. Não se preocupe, nós resolveremos isso em breve.
Criar pontos de entrada (pontos de extremidade)
Nossa API estará disponível na rota /items
. Por meio desse ponto de entrada, os usuários podem receber dados e os administradores gerenciam o menu. Vamos criá-lo.
Para fazer isso, crie um diretório chamado items
dentro do src
. Todos os arquivos associados à rota /items
serão armazenados neste novo diretório.
Criando controladores
no nest.js
, como em muitas outras estruturas, os controladores são responsáveis pelo mapeamento de rotas com funcionalidade. Para criar um controlador no nest.js
use o decorador nest.js
seguinte maneira: @Controller(${ENDPOINT})
. Além disso, para mapear vários métodos HTTP
, como GET
e POST
, são usados os decoradores @Post
, @Delete
, @Delete
etc.
No nosso caso, precisamos criar um controlador que retorne os pratos disponíveis no restaurante e que os administradores usarão para gerenciar o conteúdo do menu. Vamos criar um arquivo chamado items.controller.tc
no diretório src/items
com o seguinte conteúdo:
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 disponibilizar nosso novo controlador em nosso aplicativo, registre-o no módulo:
import { Module } from '@nestjs/common'; import { ItemsController } from './items/items.controller'; @Module({ imports: [], controllers: [ItemsController], providers: [], }) export class AppModule {}
Inicie nosso aplicativo: npm run start:dev
e abra no navegador http: // localhost: 3000 / items , se você fez tudo corretamente, veremos a resposta para a nossa solicitação get: ['Pizza', 'Coke']
.
Nota do tradutor: para criar novos controladores, assim como outros elementos do nest.js
.: serviços, provedores etc., é mais conveniente usar o comando nest generate
do nest-cli
. Por exemplo, para criar o controlador descrito acima, você pode usar o comando nest generate controller items
, como resultado do qual o nest criará src/items/items.controller.tc
src/items/items.controller.spec.tc
e src/items/items.controller.tc
seguinte conteúdo:
import { Get, Post, Controller } from '@nestjs/common'; @Controller('items') export class ItemsController {}
e registre-o em app.molule.tc
Adicionando um serviço
Agora, ao acessar /items
nosso aplicativo retorna a mesma matriz para cada solicitação, que não podemos alterar. Processar e salvar dados não é da conta do controlador; para essa finalidade, os serviços são destinados ao nest.js
Os serviços no ninho são @Injectable
O nome do decorador fala por si só, adicionando esse decorador à classe o torna injetável em outros componentes, como controladores.
Vamos criar nosso serviço. Crie o arquivo items.service.ts
na pasta items.service.ts
com o seguinte conteúdo:
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); } }
e altere o controlador ItemsController
(declarado em items.controller.ts
) para usar nosso serviço:
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); } }
Na nova versão do controlador, aplicamos o decorador @Body
ao argumento do método create
. Este argumento é usado para corresponder automaticamente os dados passados através de req.body ['item']
ao próprio argumento (nesse caso, item
).
Além disso, nosso controlador recebe uma instância da classe ItemsService
, injetada através do construtor. Declarar ItemsService
como private readonly
torna uma instância imutável e visível apenas dentro da classe.
E não se esqueça de registrar nosso serviço em 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 {}
Após todas as alterações, vamos enviar uma solicitação HTTP POST para o menu:
curl -X POST -H 'content-type: application/json' -d '{"item": "Salad"}' localhost:3000/items
Em seguida, verificaremos se novos pratos apareceram em nosso menu fazendo uma solicitação GET (ou abrindo http: // localhost: 3000 / itens em um navegador)
curl localhost:3000/items
Criando uma rota de carrinho de compras
Agora que temos a primeira versão do ponto de entrada /items
nossa API, vamos implementar a funcionalidade do carrinho de compras. O processo de criação dessa funcionalidade não é muito diferente da API já criada. Portanto, para não desorganizar o manual, criaremos um componente que responde com o status OK ao acessar.
Primeiro, na pasta ./src/shopping-cart/
crie o 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 em nosso 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 esse ponto de entrada, execute o seguinte comando, depois de verificar se o aplicativo está em execução:
curl -X POST localhost:3000/shopping-cart
Adicionando um TypeScript de interface para itens
Voltar ao nosso serviço de items
. Agora, salvamos apenas o nome do prato, mas isso claramente não é suficiente e, com certeza, queremos ter mais informações (por exemplo, o custo do prato). Eu acho que você concorda que armazenar esses dados como uma matriz de seqüências de caracteres não é uma boa ideia?
Para resolver esse problema, podemos criar uma matriz de objetos. Mas como salvar a estrutura dos objetos? Aqui a interface TypeScript nos ajudará, na qual definiremos a estrutura do objeto de items
. Crie um novo arquivo chamado item.interface.ts
na pasta src/items
:
export interface Items { readonly name: string; readonly price: number; }
Em seguida, items.service.ts
arquivo 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); } }
E também em 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); } }
Validação de entrada no Nest.js
Apesar de termos determinado a estrutura do objeto do item
, nosso aplicativo não retornará um erro se enviarmos uma solicitação POST inválida (qualquer tipo de dado não definido na interface). Por exemplo, para tal solicitação:
curl -H 'Content-Type: application/json' -d '{ "name": 3, "price": "any" }' http://localhost:3000/items
o servidor deve responder com um status de 400 (solicitação incorreta), mas, em vez disso, nosso aplicativo responderá com um status de 200 (OK).
Para resolver esse problema, crie um DTO (Data Transfer Object) e um componente Pipe (canal).
DTO é um objeto que define como os dados devem ser transferidos entre processos. Nós descrevemos o DTO no src/items/create-item.dto.ts
:
import { IsString, IsInt } from 'class-validator'; export class CreateItemDto { @IsString() readonly name: string; @IsInt() readonly price: number; }
Os pipes no Nest.js
são os componentes usados para a validação. Para nossa API, crie um canal no qual ele verifique se os dados enviados ao método correspondem ao DTO. Um canal pode ser usado por controladores diferentes, portanto, crie o diretório src/common/
com o arquivo 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: Precisamos instalar dois módulos: class-validator
class-transformer
. Para fazer isso, execute o npm install class-validator class-transformer
no console e reinicie o servidor.
Adaptando items.controller.ts
para uso com nosso novo pipe e 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); } }
Vamos verificar nosso código novamente, agora a entrada /items
aceita dados apenas se eles estiverem definidos no DTO. Por exemplo:
curl -H 'Content-Type: application/json' -d '{ "name": "Salad", "price": 3 }' http://localhost:3000/items
Cole dados inválidos (dados que não podem ser verificados no ValidationPipe
). Como resultado, obtemos a resposta:
{"statusCode":400,"error":"Bad Request","message":"Validation failed"}
Criando Middleware
De acordo com a página de guia de início rápido do Auth0 , a maneira recomendada de verificar o token JWT emitido pelo Auth0 é usar o middleware Express fornecido pelo express-jwt
. Esse middleware automatiza grande parte do trabalho.
Vamos criar um arquivo authentication.middleware.ts
dentro do diretório src / common
com o seguinte 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(); }); }; }
Substitua ${DOMAIN}
pelo valor do domínio nas configurações do aplicativo Auth0
Nota do tradutor: em um aplicativo real, retire DOMAIN
em uma constante e defina seu valor via env
(ambiente virtual)
Instale as jwks-rsa
express-jwt
e express-jwt
jwks-rsa
:
npm install express-jwt jwks-rsa
É necessário conectar o middleware criado (manipulador) ao nosso aplicativo. Para fazer isso, no arquivo ./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 }, ); } }
O código acima diz que as solicitações POST para as rotas /items
e /shopping-cart
são protegidas pelo middleware Express , que verifica o token de acesso na solicitação.
Reinicie o servidor de desenvolvimento ( npm run start:dev
) e chame a API Nest.js:
Gerenciamento de funções com Auth0
No momento, qualquer usuário com um token verificado pode postar itens em nossa API. No entanto, gostaríamos que apenas usuários com direitos de administrador possam fazer isso. Para implementar esta função, usamos as regras (regras) Auth0 .
Então, vá para o painel de controle Auth0, na seção Regras . Lá, clique no botão + CREATE RULE
e selecione "Definir funções para um usuário" como modelo de regra.

Feito isso, obtemos um arquivo JavaScript com um modelo de regra que adiciona a função de administrador a qualquer usuário com email pertencente a um determinado domínio. Vamos mudar alguns detalhes neste modelo para obter um exemplo funcional. Para nosso aplicativo, concederemos ao administrador apenas acesso ao nosso próprio endereço de email. Também precisaremos alterar o local para armazenar informações de status do administrador.
No momento, essas informações são armazenadas em um token de identificação (usado para fornecer informações sobre o usuário), mas um token de acesso deve ser usado para acessar recursos na API. O código após as alterações deve ficar assim:
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: substitua ${YOUR_EMAIL}
pelo seu endereço de email. É importante observar que, como regra, quando você lida com email nas regras Auth0, é ideal forçar a verificação por email . Nesse caso, isso não é necessário porque usamos nosso próprio endereço de email.
Nota do tradutor: o fragmento de código acima é inserido no navegador na página de configuração da regra Auth0
Para verificar se o token passado para nossa API é o token de administrador, precisamos criar um protetor Nest.js. Na pasta src/common
, crie o arquivo 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; } }
Agora, se repetirmos o processo de login descrito acima e usarmos o endereço de email definido na regra, obteremos um novo access_token
. Para verificar o conteúdo deste access_token
, copie e cole o token no campo Encoded
do site https://jwt.io/
. Veremos que a seção de carga útil desse token contém a seguinte matriz:
"http://localhost:3000/roles": [ "admin" ]
Se nosso token incluir realmente essas informações, continuaremos a integração com o Auth0. Então, abra items.controller.ts
e adicione nosso novo protetor lá:
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); } }
Agora, com nosso novo token, podemos adicionar novos itens por meio de nossa API:
Nota do tradutor: para verificação, você pode ver o que temos nos itens:
curl -X GET http://localhost:3000/items
Sumário
Parabéns! Acabamos de criar nossa API Nest.JS e agora podemos nos concentrar no desenvolvimento da parte front-end do nosso aplicativo! Verifique a segunda parte desta série: Aplicativos TypeScript de pilha completa - Parte 2: Desenvolvendo aplicativos angulares de front-end.
Nota do tradutor: A tradução da segunda parte está em andamento.
Para resumir, neste artigo, usamos vários recursos do Nest.js e TypeScript: módulos, controladores, serviços, interfaces, pipes, middleware e guarda para criar API Espero que você tenha uma boa experiência e esteja pronto para continuar desenvolvendo nosso aplicativo. Se algo não estiver claro para você, a documentação oficial do nest.js. é uma boa fonte com respostas