Aplicativos TypeScript de pilha completa

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 # v12.11.1 npm -v # 6.11.3 

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:


  #    cd nest-restaurant-api #   npm run start:dev 

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ódulo
  • src/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:


  #     curl -X POST http://localhost:3000/shopping-cart #      TOKEN="eyJ0eXAiO...Mh0dpeNpg" # and issue a POST request with it curl -X POST -H 'authorization: Bearer '$TOKEN http://localhost:3000/shopping-cart 

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:


  #    npm run start:dev #  POST       curl -X POST -H 'Content-Type: application/json' \ -H 'authorization: Bearer '$TOKEN -d '{ "name": "Salad", "price": 3 }' http://localhost:3000/items 

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

Source: https://habr.com/ru/post/pt471348/


All Articles