Desenvolvimento Back-end Node Guias

Guia do framework NestJS

Neste guia do framework NestJS vamos fazer um compilado de tópicos úteis sobre o framework, desde criar um projeto básico até autenticação.

há 9 meses 1 semana

Formação Desenvolvedor Node Full-Stack
Conheça a formação em detalhes

Neste guia completo veremos como utilizar o NestJS para criar aplicações back-end e fullstack de forma prática.

O que é o NestJS

O NestJS é um framework back-end que auxilia o desenvolvimento de aplicações Node.js, ele utiliza como padrão o TypeScript e possui uma sintaxe familiar ao Angular. Por padrão, trabalha com o Express, mas também trás a possibilidade de trabalhar com o Fastfy, tornando mais prático o desenvolvimento de aplicações eficientes, escaláveis e robustas.

TypeScript - Fundamentos
Curso TypeScript - Fundamentos
Conhecer o curso

Criando um projeto NestJS

Para criar um projeto NestJS é necessário termos o ambiente Node.js instalando e configurado em nossa máquina, você pode consultar um passo a passo de Instalação do Node.js no artigo: Node.js - Windows, Mac e Linux.

Com o ambiente Node.js configurado, podemos instalar a ferramenta NestCLI, que irá auxiliar na criação de projetos e arquivos, com o seguinte comando:

npm i -g @nestjs/cli

Após a instalação, para criar efetivamente um projeto, vamos executar o comando:

nest new nome-do-projeto

Desta forma teremos a estrutura de um projeto base iniciado para começar o desenvolvimento de uma aplicação utilizando o NestJS, com uma estrutura similar a imagem abaixo:

Estrutura NestJS

Para executar um projeto NestJS basta utilizar o comando:

npm run start:dev

E acessar localhost:3000 (porta padrão) para acessar a aplicação.

Mais detalhes podem ser consultados em nosso artigo “O que é NestJS?

Criando primeiro CRUD com NestJS

Para criar o primeiro CRUD com NestJS, vamos criar um projeto conforme o comando acima, veja que temos a estrutura igual aos arquivos da imagem acima, desta forma teremos alguns arquivos e camadas que serão respectivos a:

  • Entity: são os modelos da aplicação que irão espelhar as tabelas do banco de dados;
  • Controller: é a camada responsável pelas rotas, respostas e requisições;
  • Service: é a camada responsável pela aplicação das regras de negócio;
  • Repository: é a camada responsável pela comunicação com o banco de dados.

Podemos utilizar o comando nest g res users --no-spec para criar os arquivos responsáveis por cada responsabilidade automaticamente, neste caso, onde teremos um recurso referente a usuários. A flag --no-spec é utilizada para não gerar os arquivos de teste, que no momento não serão utilizados.

Definindo entidade

Para difnir uma entidade, vamos localizar o arquivo user.entity.ts, neste arquivo podemos adicionar as propriedades que serão mapeadas para colunas respectivamente no banco de dados, um exemplo seria:

export class User {
  id: number;
  nome: string;
  idade: number;
}

Agora, o próximo passo é configurar o TypeORM, que será o ORM utilizado neste exemplo, onde fará o papel de espelhar a entidade para o Banco de Dados, para isto, vamos executar o comando de instalação:

npm i @nestjs/typeorm typeorm sqlite3

Veja que instalamos o TypeORM e o SQLite. Após a instalação podemos adaptar a entidade para que o TypeORM consiga identificar a entidade e as propriedades:

import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class User {
  @PrimaryGeneratedColumn('increment')
  id: number;

  @Column()
  nome: string;

  @Column()
  idade: number;
}

Em seguida será necessário criar um arquivo de configuração, nomeado aqui de ormconfig.ts onde vamos apontar o banco de dados que será utilizado, os dados de acesso e onde estarão as entidades:

import { DataSourceOptions } from 'typeorm';

export const config: DataSourceOptions = {
  type: 'sqlite',
  database: '.db/sql',
  synchronize: true, // Obs: use synchronize: true somente em desenvolvimento.
  entities: [__dirname + '/**/*.entity{.ts,.js}'],
};

Por útimo, é necessário importar no módulo de usuário que estamos utilizando o TypeORM:

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UsersModule } from './users/users.module';
import { config } from './ormconfig';

@Module({
  imports: [UsersModule, TypeOrmModule.forRoot(config)],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Desenvolvendo o CRUD (create, read, update, delete)

Primeiramente devemos atualizar o arquivo create-user.dto que foi criado pelo NestCLI, lembrando que ele terá somente as propriedades nome e idade, pois o id será gerado automaticamente pelo banco:

export class CreateUserDto {
  nome: string;
  idade: number;
}

Create - cadastro de usuários

Agora podemos desenvolver a função ‘create’, ou seja. vamos cadastrar um novo usuário, de acordo com os padrões de uma API REST e do protocolo HTTP, é utilizado o verbo HTTP POST. Portanto, vamos configurar os arquivos responsáveis pelo controller, service e repositório.

Controller:

@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Post()
  create(@Body() createUserDto: CreateUserDto) {
    return this.usersService.create(createUserDto);
  }
}

Service e Repositório:

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private userRepository: Repository<User>,
  ) {}

  create(createUserDto: CreateUserDto) {
    return this.userRepository.save(createUserDto);
  }
}

Ao fazer uma requisição para esta rota passando um JSON no corpo da requisição referente ao usuário, será efetuado o cadastro e retornado o usuário cadastrado: criando usuario

Update - Atualização de usuários

Para atualizar um registro podemos utilizar o método PUT (atualizar todo o registro), ou PATCH (atualização parcial do registro), neste caso vamos utilizar a rota PATCH. Vale ressaltar que o arquivo uptade-user.dto está estendendo o arquivo create-user.dto, portanto, não será necessario atualiza-lo já que não teremos nenhuma alteração em relação as propriedades do registro. Desta forma, teremos os seguintes métodos que serão adicionados nas camadas de controller, service e repositório:

Controller

@Patch(':id')
update(@Param('id') id: number, @Body() updateUserDto: UpdateUserDto) {
  return this.usersService.update(id, updateUserDto);
}

Service e repositório:

update(id: number, updateUserDto: UpdateUserDto) {
    return this.userRepository.update(id, updateUserDto);
}

Retorno da rota patch atualizando usuário

TypeScript - Avançado
Curso TypeScript - Avançado
Conhecer o curso

Read - Busca e Lista de usuários

Podemos buscar um registro ou listar todos os registros. Para isto vamos implementar duas rotas utilizando método http GET, onde uma irá simplesmente retornar todos os usuários, entretanto, a outra irá retornar somente um registro, que será referente ao ID do registro que vamos passar na URL.

Controller:

@Get()
findAll() {
  return this.usersService.findAll();
}

@Get(':id')
findOne(@Param('id') id: number) {
  return this.usersService.findOne(id); //veja que vamos receber o id na rota e 
}                                       //passar como parametro no método findOne

Service e repositório:

findAll() {
  return this.userRepository.find();
}

findOne(id: number) {
  return this.userRepository.findOneBy({ id: id }); //veja que passamos o id
}                                                   //para buscar o registro

Neste caso podemos observar o retorna da lista de usuários: Retorno da lista de registos

E da buscar por um único registro: Retorno da busca única de registro

Vale salientar que para fazer a busca pela lista será pela rota GET “http://localhost:3000/users”, e para buscar por ID será pela rota GET “http://localhost:3000/users/1”, sendo 1 o ID respectivo ao usuário a ser consultado.

Delete - excluir usuários

E por último, vamos implementar a função de excluir um registro, onde utilizamos o método HTTP DELETE.

Controller:

@Delete(':id')
remove(@Param('id') id: number) {
  return this.usersService.remove(id);
}

Service e repisório:

remove(id: number) {
  return this.userRepository.delete(id);
}

Retorno da exclusão de registro: Retorno da exclusão de registro

A rota utilizada para excluir o registro será a rota DELETE “http://localhost:3000/users/1”, sendo 1 o ID respectivo ao registro a ser excluído.

E a base de um CRUD simples está concluída, caso você se interesse em se aprofundar na criação desse CRUD temos o artigo dedicado somente a este exemplo em criando o primeiro CRUD com NestJS.

Validação e Serialização

Outro ponto impontante quando estamos desenvolvendo uma aplicação é validar os dados que serão enviados para a aplicação e parsear esse dados quando necessário. No NestJS temos dois pacotes que nos ajudam a trabalhar com esses recursos, sendo eles o class-validator e class-transformer.

Para instalar esses pacotes, basta executar o seguinte comando:

npm i class-validator class-transformer

Class Validator

Uma forma simples e eficiente de utilizar o class-validator é através da utilização de decorators para validar as propriedades de um objeto, vamos ver um exemplo utilizando no arquivo create-user.dto, que será a classe responsável por transportar os dados da requisição até a persistencia do objeto em si no banco, veja um exemplo:

import { IsNotEmpty, Length } from 'class-validator';

export class CreateUserDto {
  @IsNotEmpty({ message: 'Nome não pode ser vazio' })
  @Length(3, 10, { message: 'Nome precisa ter entre 3 e 10 caracteres' })
  nome: string;

  @IsNotEmpty()
  idade: number;
}

Veja que utilizamos o decorator @IsNotEmpty() para verificar se a propriedade nome é vazia. nula ou indefinida. Caso positivo, exibirá a mensagem que a propriedade não pode ser vazia.

Basicamente as validações seguirão esse padrão, um decorator que irá fazer a validação com um nome intuitivo e com a possibilidade de receber parametros e opções, como mensagens personalizadas, ou definir qual padrão de modelo de data para uma propriedade Date, por exemplo.

Vale salientar que devemos configurar no main.ts a utilização da validação de forma global:

import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe()); //devemos passar essa configuração
  await app.listen(3000);
}
bootstrap();

Class Transformer

Agora podemos utilizar o class-transformer para manipular essa informação, por exemplo, vamos imaginar um cenário que os nomes devem estar todos com letras minúsculas, para isso podemos usar o decorator @Transform() nos DTOs, conforme abaixo:

import { Transform } from 'class-transformer';
import { IsNotEmpty, Length, Validate } from 'class-validator';
import { IdadeValida } from '../validator-idade';

export class CreateUserDto {
  @IsNotEmpty({ message: 'Nome não pode ser vazio' })
  @Length(3, 10, { message: 'Nome precisa ter entre 3 e 10 caracteres' })
  @Transform(({ value }) => value.toLowerCase())
  nome: string;

  @IsNotEmpty()
  @Validate(IdadeValida)
  idade: number;
}

Utilizamos o decorator @Transform() para converter todos os caracteres da propriedade nome em letras minusculas. Outro ponto também é que podemos utilizar os recursos @Exclude() e @Expose() onde é possível controlar quando e como vamos capturar ou exibir uma propriedade tanto na requisição quanto na resposta, por exemplo:

import { Exclude, Expose, Transform } from 'class-transformer';
import { IsNotEmpty, Length, Validate } from 'class-validator';
import { IdadeValida } from '../validator-idade';

export class CreateUserDto {
  @IsNotEmpty({ message: 'Nome não pode ser vazio' })
  @Length(3, 10, { message: 'Nome precisa ter entre 3 e 10 caracteres' })
  @Transform(({ value }) => value.toLowerCase())
  @Expose({ name: 'nome_completo' })
  nome: string;

  @IsNotEmpty()
  idade: number;

  @Exclude()
  id: number;
}

Desta forma, a propriedade que será considerada como ‘nome’ no corpo da requisição terá que ser enviada como ‘nome_completo’, e ao retornar esse dto, veja que a propriedade id está com o decorator @Exclude, portanto, ela será ignorada e a propriedade nome também será exibida como ’nome_completo´.

Por fim, também é necessário mais uma configuração no arquivo main.ts para que a serialização seja efetuada de forma global na aplicação:

import { ClassSerializerInterceptor, ValidationPipe } from '@nestjs/common';
import { NestFactory, Reflector } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  //uso global de interceptor, e a serialização dos dados nas requisições.
  app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));
  app.useGlobalPipes(new ValidationPipe({ transform: true })); 
  await app.listen(3000);
}
bootstrap();

Aqui vimos o básico do class-validator e class-transformer, porém são recursos que possuem inúmeras funcionalidades, algumas delas abordamos no artigo validação e serialização no NestJS.

Node.js - Fundamentos
Curso Node.js - Fundamentos
Conhecer o curso

NestJS com template engine handlebars

Podemos utilizar templates engines para criar aplicações full stack com o NestJS, uma delas é o Handlebars. Agora veremos como utiliza-la.

Primeiramente vamos instalar o Handlebars, com o comando:

npm i hbs

É necessário configurar a utilização do template engine e também apontar em qual diretório estarão as views, que serão os arquivos referentes as nossas páginas. Essa configuração deve ser feita no arquivo main.ts:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { NestExpressApplication } from '@nestjs/platform-express';
import { join } from 'path';

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule);

  app.useStaticAssets(join(__dirname, '..', 'public')); //arquivos públicos
  app.setBaseViewsDir(join(__dirname, '..', 'views')); //views
  app.setViewEngine('hbs');

  await app.listen(3000);
}
bootstrap();

Criando e renderizando uma view

Neste momento podemos começar a desenvolver nossas views, vamos criar um diretório chamado views na raiz do projeto e criar um arquivo chamado index.hbs:

<!DOCTYPE html>
<html>
  <head>
    <title>Uma View com Handlebars</title>
  </head>
  <body>
    <h1>NestJS com Handlebars!</h1>
  </body>
</html>

Precisamos criar uma rota para a view, e também utilizar o decorator @Render(), desta forma quando houver uma requisição para esta rota será renderizado a view respectiva:

import { Controller, Get, Render } from '@nestjs/common';

@Controller()
export class AppController {
  @Get()
  @Render('index')
  index() {
    //
  }
}

Ao executar e acessar a aplicação, teremos o seguinte resultado:

Tela com o texto nestjs com handlebars sendo exibido

Para ver outros recursos do Handlebars você pode acessar o artigo NestJS com template engine Handlebars.

Nest.js - Templates com Handlebars
Curso Nest.js - Templates com Handlebars
Conhecer o curso

Como enviar emails com NestJS e Nodemailer

Primeiramente, crie um novo projeto com o NestCLI, conforme descrito no ínicio deste guia.

Agora, podemos utilizar o módulo Nodemailer para nos auxiliar no envio de emails utilizando o NestJS, para isto vamos primeiro fazer a sua instalação executando os comandos:

npm i @nestjs-modules/mailer nodemailer
npm i --save-dev @types/nodemailer

Ótimo, agora precisamos configurar um serviço de envio de emails, neste exemplo vamos utilizar o mailgun, onde podemos criar uma conta gratuita para testes. Lembrando que NÃO é necessario colocar método de pagamento no cadastro.

A conta de teste irá permetir que enviemos emails para endereços cadastrados, portanto, cadastre um email que será utilizado para essa finalidade. O mailgun irá fornecer um usuário e senha para a utilização de envio de emails via SMTP, guarde essas informações.

O próximo passo será configurar o mailgun em nossa aplicação, para isto, vamos importar o MailModule no arquivo app.module.ts:

@Module({
  imports: [
    MailerModule.forRoot({
      transport: {
        host: 'smtp.mailgun.org', //host smtp
        secure: false, //regras de segurança do serviço smtp
        port: 587, // porta
        auth: { //dados do usuário e senha
          user: 'seu-usuario',
          pass: 'sua-senha',
        },
        ignoreTLS: true,
      },
      defaults: { // configurações que podem ser padrões
        from: '"',
      },
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Criar o método enviarEmail() no arquivo app.service.ts:

import { MailerService } from '@nestjs-modules/mailer';
import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService {
  constructor(private mailerService: MailerService) {}

  async enviarEmail(email: string, mensagem: string) {
    await this.mailerService.sendMail({
      to: email, //email de destino, lembre-se de passar o email cadastrado no mailgun
      from: 'wesley.gado@treinaweb.com.br',
      subject: 'Enviando Email com NestJS',
      html: `<h3 style="color: red">${mensagem}</h3>`,
    });
  }
}

Agora vamos criar uma view onde estará o formulário para envio de email, para isto, vamos fazer o processo do tópico acima de instalação e configuração do Handlebars e então criar a view forms.hbs:

<html lang="pt-br">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Envio de Email</title>
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.1/dist/css/bootstrap.min.css" rel="stylesheet"integrity="sha384-iYQeCzEYFbKjA/T2uDLTpkwGzCiq6soy8tYaI1GyVh/UjpbCx/TYkiZhlZB6+fzT" crossorigin="anonymous">

</head>

<body>
  <div class="container">
    <div class="mb-3 mt-3">
          <h3>Envio de emails utilizando NestJS</h3>
    <form action="/enviar-email" method="post">
      <div class="col-md-4">
        <input class="form-control mb-3" type="email" placeholder="Email" name="email">
      </div>
      <div class="col-md-4">
        <textarea class="form-control mb-3" type="text" placeholder="Mensagem" name="mensagem"></textarea>
      </div>
      <button class="btn btn-primary" type="submit">Enviar Email</button>
    </form>
    </div>
  </div>
</body>

</html>

E por fim, precisamos configrar as rotas, tanto a rota GET para acessar a view referente ao formulário, quanto a rota POST que será responsável por enviar os dados do formuário para o nodemailer:

import { Controller, Get, Post, Render, Request } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get('email')
  @Render('form')
  exibirForm() { //acessar página de formulário
  //
  }

  @Post('enviar-email')
  enviarEmail(@Request() req) { //enviar dados do formulário para o nodemailer
    return this.appService.enviarEmail(
      req.body.email,
      req.body.mensagem,
    );
  }
}

Desta forma a aplicação será capaz de enviar emails utilizando um servidor SMTP.

Caso você queira mais detalhes sobre a criação da conta no mailgun e o passo a passo de toda a configuração, temos o artigo completo Como enviar emails com NestJS e Nodemailer.

Express - Desenvolvendo aplicações web
Curso Express - Desenvolvendo aplicações web
Conhecer o curso

Upload local de arquivos com NestJS

Nesse tópico veremos como fazer upload de arquivos localmente com NestJS, para começar, vamos criar um novo projeto e configurar um banco de dados conforme o inicio desse guia, logo em seguida vamos criar o recurso files com o seguinte comando:

nest g res files --no-spec

Perfeito, agora devemos configurar a entidade files:

import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class File {
  @PrimaryGeneratedColumn('increment')
  id: number;

  @Column({ nullable: false, unique: true })
  fileName: string;

  @Column({ nullable: false })
  contentLength: number;

  @Column({ nullable: false })
  contentType: string;

  @Column({ nullable: false })
  url: string;
}

Desse modo vamos salvar algumas propriedades do arquivo no banco de dados, são elas:

  • id: o número de identificação do arquivo;
  • fileName: o nome do arquivo;
  • contentLength: o tamanho do arquivo;
  • contentType: o mimetype que identifica do arquivo;
  • url: a url gerada para acessar o arquivo, caso seja necessário em uma aplicação web por exemplo.

Vamos utilizar o módulo Multer, onde podemos consultar algumas especificações na própria documentação do NestJS, para realizar o upload dos arquivos. Para instala-lo basta executar o comando:

npm i -D @types/multer

Agora é necessário configurar o multer, onde vamos criar uma constate com essas configurações em um arquivo que tem somente essa responsabilidade, o multer-config.ts:

import { diskStorage } from 'multer';
import * as path from 'path';
import { v4 as uuidv4 } from 'uuid';

const multerConfig = {
  storage: diskStorage({
    destination: './upload/files',
    filename: (req, file, cb) => {
      const fileName =
        path.parse(file.originalname).name.replace(/\s/g, '') + '-' + uuidv4();

      const extension = path.parse(file.originalname).ext;
      cb(null, `${fileName}${extension}`);
    },
  }),
};

export default multerConfig;

Basicamente, indicamos que o arquivo será salvo localmente, passamos o diretório do arquivo e adicionamos um nome único para o arquivo.

Devemos criar o método na camada de serviço para salvar os dados, desta forma:

import { Injectable } from '@nestjs/common';
import { Repository } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm';
import { File } from './entities/file.entity';
import { Request } from 'express';

@Injectable()
export class FilesService {
  constructor(
    @InjectRepository(File)
    private fotoRepository: Repository,
  ) {}
  async salvarDados(file: Express.Multer.File, req: Request) {
    const arquivo = new File();
    arquivo.fileName = file.filename;
    arquivo.contentLength = file.size;
    arquivo.contentType = file.mimetype;
    arquivo.url = `${req.protocol}://${req.get('host')}/files/${file.filename}`;

    return await this.fotoRepository.save(arquivo);
  }
}

Agora precisamos configurar o controller, onde vamos receber o arquivo propriamente dito e os dados deste arquivo que serão salvos no banco de dados.

Portanto, vamos criar uma rota files que será uma rota POST responsável por enviar os dados e o arquivo para upload:

import {
  Controller,
  Post,
  UseInterceptors,
  UploadedFile,
  Req,
} from '@nestjs/common';
import { FilesService } from './files.service';
import { FileInterceptor } from '@nestjs/platform-express';
import multerConfig from './multer-config';
import { Request } from 'express';

@Controller('files')
export class FilesController {
  constructor(private readonly filesService: FilesService) {}

  @Post()
  @UseInterceptors(FileInterceptor('arquivo', multerConfig))
  uploadArquivo(
    @UploadedFile() file: Express.Multer.File,
    @Req() req: Request,
  ) {
    return this.filesService.salvarDados(file, req);
  }
}

Podemos testar fazendo o upload de um arquivo utilizando o insomnia, configurando a rota POST para http://localhost:3000/files, lembrando que devemos selecionar o body referente a Multipart Form, colocar o nome da propriedade como ‘arquivo’ e então selecionar file para o tipo de arquivo, por último, selecionar o arquivo que será efetuado o upload.

Selecionando arquivo para envio no formulário

Então podemos realizar a requisição, se tudo ocorrer corretamente, vamos obter o retorno 201 created, será salvo no banco os dados os dados arquivo, e o arquivo no diretório que configuramos no multer.

Para ver mais detalhes de como realizar o upload no NestJS, e até a possibilidade de fazer upload de múltiplos arquivos, entre em nosso artigo mais aprofundado Como realizar upload de arquivos locais no NestJS.

Upload de arquivos no S3

Agora podemos ver como realizar o upload de imagem para a AWS S3, neste cenário vamos utilizar o Multer s3, uma variação do Multer especifica para essa finalidade, e o SDK da AWS. Então, vamos executar o seguinte comando:

npm i aws-sdk multer-s3
npm i -D @types/multer-s3

Perfeito, será necessario atualizarmos o arquivo multer-config.ts, ou podemos criar outro similar porém com as alterações abaixo:

import * as multerS3 from 'multer-s3';
import { S3Client } from '@aws-sdk/client-s3';
import * as path from 'path';
import { v4 as uuidv4 } from 'uuid';

const s3Config = new S3Client({
  region: 'sa-east-1', //região selecionada na criação do bucket
  credentials: {
    accessKeyId: 'chave de acesso', //chave de acesso
    secretAccessKey: 'chave de acesso secreta', //chave de acesso secreta
  },
});

const multerConfig = {
  storage: multerS3({
    s3: s3Config,
    bucket: 'artigo-tw-s3',
    contentType: multerS3.AUTO_CONTENT_TYPE,
    acl: 'public-read',
    key: (req, file, cb) => {
      const fileName =
        path.parse(file.originalname).name.replace(/\s/g, '') + '-' + uuidv4();

      const extension = path.parse(file.originalname).ext;
      cb(null, `${fileName}${extension}`);
    },
  }),
};

Veja que neste caso temos duas cosntantes, uma com as configurações do S3, e a constante multerConfig com as alterações relacionadas a mudança do destino do arquivo, no caso a AWS S3.

Também será necessario atualizar o Controller, já que agora estamos utilizando o Multer S3:

import {
  Controller,
  Post,
  UseInterceptors,
  UploadedFile,
  UploadedFiles,
} from '@nestjs/common';
import { FilesService } from './files.service';
import {
  FileFieldsInterceptor,
  FileInterceptor,
} from '@nestjs/platform-express';
import multerConfig from './multer-config';

@Controller('files')
export class FilesController {
  constructor(private readonly filesService: FilesService) {}

  @Post()
  @UseInterceptors(FileInterceptor('arquivo', multerConfig))
  uploadArquivo(@UploadedFile() file: Express.MulterS3.File) {
    console.log(file);
    return this.filesService.salvarDados(file);
  }
}

Perfeito, podemos testar utilizando o insomnia novamente, desta forma teremos os dados do arquivo salvos no banco de dados localmente, mas teremos o arquivo salvo na AWS S3.

Caso você queira realizar o upload de vários arquivos, ou não tenha familiaridade com a AWS S3, veja nosso artigo detalhado com a criação da conta no S3 e outros rescursos em Como realizar upload no S3 com NestJS.

Autenticação: Geração de Token JWT com NestJS e Passport.

Pedemos deixar nossas aplicações seguras utilizando autenticação JWT e para isto vamos utilizar a biblioteca do Passport que é recomendada pela própria documentação do NestJS, que é uma biblioteca muito utilizada no escossistema Node.js.

Primeiramente para criarmos uma aplicação novamente, vamos utilizar o passo a passo do inicio deste guia para criar um novo projeto com o NestCLI e configurar o banco de dados. Após, podemos criar um recurso Users, e então, criar a entidade e o DTO:

//user.entity.ts
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class User {
  @PrimaryGeneratedColumn('increment')
  id: number;

  @Column()
  nome: string;

  @Column()
  email: string;

  @Column()
  password: string;
}
//create-user.dto.ts
export class CreateUserDto {
  nome: string;
  email: string;
  password: string;
}

Vale salientar que em uma [aplicação real é importante utilizar meios de criptografia de senha, como o bcrypt]. (https://docs.nestjs.com/security/encryption-and-hashing).

Perfeito, agora o segundo passo é instalar o Passport:

npm install @nestjs/jwt passport-jwt @nestjs/passport passport passport-local
npm install --save-dev @types/passport-jwt @types/passport-local

Agora temos os pacotes necessários para a implementação do JWT. Logo em seguida vamos criar o diretório auth em src, criar um módulo em /auth o seus respectivos service e controller:

nest g module auth
nest g service auth
nest g controller auth

Com os arquivos criados, vamos configurar o auth.service.ts, desta forma:

//auth.service.ts
import {
  Injectable,
  NotAcceptableException,
  UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { User } from 'src/users/entities/user.entity';
import { UsersService } from 'src/users/users.service';

@Injectable()
export class AuthService {
  constructor(
    private readonly usersService: UsersService,
    private jwtService: JwtService,
  ) {}

//validar usuário
  async validarUsuario(username: string, password: string): Promise<any> {
    const user = await this.usersService.findOneByEmail(username);
    if (!user) {
      throw new UnauthorizedException('Usuário ou Senha Inválidos');
    }
    if (user.password === password) {
      return await this.gerarToken(user);
    }
    throw new UnauthorizedException('Usuário ou Senha Inválidos');
  }

//gerar JWT Token
  async gerarToken(payload: User) {
    return {
      access_token: this.jwtService.sign(
        { email: payload.email },
        {
          secret: 'topSecret512',
          expiresIn: '50s',
        },
      ),
    };
  }
}

No método gerarToken vamos receber o usuário caso esteja tudo ok, e então utilizaremos o jwtService.sign, esse método faz parte do pacote do Passporte, que permite gerar tokens com diversas opções, neste caso, vamos utilizar duas propriedades:

  • secret: É uma key que será utilizada para gerer o Token, o que garante a segurança do Token JWT de forma que outra aplicação não consiga gerar o mesmo Token JWT.
  • expiresIn: O tempo de duração do Token, podemos selecionar um tempo para que esse token deixe de ser válido, desta forma sendo necessário gerar outro token, permitindo que não seja utilizado de forma maliciosa caso algum atacante tenha acesso a esse Token.

Obs: no artigo passamos a secret direto na propriedade, porém é importante que para esses dados sensíveis sejam utilizados as variáveis de ambiente.

Outro ponto que não pode passar desapercebido é que temos o método findOnByEmail recebendo o username como parâmetro, porém não temos ele em nosso usersService, para isto, vamos criar um novo método de busca de usuário por email no arquivo users.service.ts:

//users.service.ts
//[...]
findOneByEmail(username: string) {
    return this.userRepository.findOneBy({ email: username });
  }
//[...]

Ótimo! O arquivo auth.service.ts está configurado.

A próxima etapa é criar o arquivo local.auth.ts, neste arquivo vamos configurar a estratégia de autenticação do Passport, ele possibilita a configuração de várias estratégias que podem ser consultadas na sua documentação e então escolhidas conforme a sua necessidade. Neste caso vamos selecionar a estratégia local e o JWT, o arquivo local.auth.ts ficará da seguinte forma:

//local.auth.ts
import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthService } from './auth.service';

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
  constructor(private authService: AuthService) {
    super();
  }

  async validate(username: string, password: string): Promise<any> {
    const user = await this.authService.validateUser(username, password);
    if (!user) {
      throw new UnauthorizedException();
    }
    return user;
  }
}

Com o auth.service e local.auth.ts configurados, precisamos adicionar os imports e providers no módulo auth:

//auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from 'src/users/entities/user.entity';
import { UsersService } from 'src/users/users.service';
import { AuthService } from './auth.service';
import { LocalStrategy } from './local-strategy';

@Module({
  imports: [
    TypeOrmModule.forFeature([User]),
    PassportModule,
    JwtModule,
  ],
  providers: [AuthService, UsersService, LocalStrategy],
})
export class AuthModule {}

No módulo basicamente vamos importar o TypeOrmModule pois precisamos utilizar a busca do usuário no banco, o PassportModule e o JwtModule que são os responsáveis pela autenticação. Nos providers vamos utilizar o UsersService e o LocalStrategy, que devem ser adicionados.

Neste momento precisamos configurar a rota no auth.controller:

//auth.controller.ts
import { Controller, Post, Body } from '@nestjs/common';
import { AuthService } from './auth.service';

@Controller()
export class AuthController {
  constructor(private authService: AuthService) {}

  @Post('auth/login')
  async login(@Body() body) {
    return this.authService.validarUsuario(body.username, body.pass);
  }
}

Portanto teremos a rota POST para /auth/login, passando o username e pass via body chamando o método validarUsuario do authService para efetuar a validação e geração do Token.

OBS COLOCAR EXEMPLO

Para mais detalhes você pode acessar o artigo Autenticação: Geração de Token JWT com NestJS e Passport.

Node.js - Templates com PUG
Curso Node.js - Templates com PUG
Conhecer o curso

Protegendo rotas com NestJS e Passport

Utilizando o exemplo acima, vamos dar continuidade em relação a autenticação, agora que podemos gerar tokens JWT para usuários autenticados, vamos proteger as rotas de forma que somente esses usuários possam ter o devido acesso.

Primeira rota que podemos criar é a rota de listagem de usuários, que irá retornar uma lista de todos os usuários cadastrados na aplicação.

Portanto, vamos configurar um método no service que retorne esta lista:

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private userRepository: Repository<User>,
  ) {}
  create(createUserDto: CreateUserDto) {
    return this.userRepository.save(createUserDto);
  }

  findAll() {
    return {
      usuarios: [
        {
          nome: 'Paulo',
          endereco: 'av paulista',
          telefone: '1155555555',
        },
        {
          nome: 'Maria',
          endereco: 'av faria lima',
          telefone: '1155555580',
        },
        {
          nome: 'Samantha',
          endereco: 'av paulista',
          telefone: '1155555590',
        },
      ],
    };
  }
	
	async findOneByEmail(username: string) {
    return await this.userRepository.findOneBy({ email: username });
  }
}

Agora vamos criar a rota no users.controller.ts:

//users.controller.ts
import { Controller, Get, Post, Body, UseGuards } from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { AuthGuard } from '@nestjs/passport';

@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Post()
  async create(@Body() createUserDto: CreateUserDto) {
    return await this.usersService.create(createUserDto);
  }

  @Get()
  @UseGuards(AuthGuard('jwt')) // Decorator responsável pelo Guard
  listarUsuarios() {
    return this.usersService.findAll();
  }
}

Foi criada uma rota GET que retorna a lista de usuários, sendo necessário o uso do decorator @UseGuards() para aplicar os Guards responsáveis por autenticação e autorização.

É possível criar Guards específicos para diferentes necessidades, como o AuthGuard fornecido pelo Passport, que permite especificar a estratégia utilizada, como o JWT.

Além disso, é necessário atualizar o arquivo local.auth.ts, renomeando-o para jwt-strategy.ts e alterando o nome da classe para JwtStrategy, realizando algumas modificações correspondentes.

//jwt-strategy.ts
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { JwtPayload } from './jwt-payload';
import { UsersService } from 'src/users/users.service';
import { User } from 'src/users/entities/user.entity';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
  constructor(private usersService: UsersService) {
    super({
      secretOrKey: 'sua-chave',
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
    });
  }
  async validate(payload: JwtPayload): Promise<User> {
    const { email } = payload;
    const user = await this.usersService.findOneByEmail(email);
    if (!user) {
      throw new UnauthorizedException();
    }
    return user;
  }
}

Obs.: Confira onde estava a importação do LocalStrategy para atualizar para JwtStrategy.

Após essas alterações, veja que no método validate temos o tipo JwtPayload, portanto, vamos criar uma interface que terá como propriedade email:

//jwt-payload.ts
export interface JwtPayload {
  email: string;
}

Finalmente podemos testar se a rota estará protegida:

  • Vamos fazer uma requisição utilizando o método POST http://localhost:3000/auth/login com os dados de login de um usuário, desta forma será retornado um token JWT.
  • Agora vamos acessar a rota de listagem de usuários GET http://localhost:3000/users, para isso precisamos copiar o token e passar na requisição. No insomnia basta clicar na opção Auth, selecionar Bearer e adicionar o Token.

Desta forma iremos ter como retorno a lista de usuários, agora teste fazer esse processo sem passar o token na requisição, vamos ter como resposta um erro 401 Unauthorized.

Você pode ver sobre esse tópico mais detalhes no artigo Autenticação: Protegendo Rotas com NestJS e Passport.

Refresh Token

Uma estratégia para que não seja necessário efetuar o login novamente e que evita o tráfego de dados sensíveis nas requisições é o uso de refresh tokens. Com eles o servidor substitui o token expirado por um novo token de acesso utilizando um refresh token para gerar um novo token, lembrando que esse refresh token também expira, portanto, ele deve ter uma duração maior que o token de acesso.

Agora precisamos então configurar a aplicação para gerar esse refresh token junto ao token de acesso. No méotodo gerarToken(), vamos adicionar a constante refresh token:

async gerarToken(payload: User) {
    const accessToken = this.jwtService.sign(
      { email: payload.email },
      {
        secret: 'sua-chave',
        expiresIn: '30s',
      },
    );

    const refreshToken = this.jwtService.sign(
      { email: payload.email },
      {
        secret: 'sua-chave-refresh',
        expiresIn: '60s', //veja o tempo de validade maior.
      },
    );
    return { access_token: accessToken, refresh_token: refreshToken };
  }

Ótimo, já estamos gerando o refresh token, porém é necessario verificar a validade desse refresh token, vamos implementar esse método no auth.service.ts, desta maneira:

async reautenticar(body) {
    const payload: User = await this.verificarRefrestToken(body); ////este método também será implementado abaixo
    return this.gerarToken(payload);
  }

Agora precisamos implementar o método verificarRefreshToken():

private async verificarRefreshToken(body) {
    const refreshToken = body.refresh_token;

    if (!refreshToken) {
      throw new NotFoundException('Usuário não encontrado');
    }

    const email = this.jwtService.decode(refreshToken)['email'];
    const usuario = await this.usersService.findOneByEmail(email);

    if (!usuario) {
      throw new NotFoundException('Usuário não encontrado');
    }

    try {
      this.jwtService.verify(refreshToken, {
        secret: 'sua-chave-refresh',
      });
      return usuario;
    } catch (err) {
      if (err.name === 'JsonWebTokenError') {
        throw new UnauthorizedException('Assinatura Inválida');
      }
      if (err.name === 'TokenExpiredError') {
        throw new UnauthorizedException('Token Expirado');
      }
      throw new UnauthorizedException(err.name);
    }
  }
}

Basicamente vamos verificar se o usuário existe em nossa base de dados e se o token é válido, ou seja, se a secrect key bate com a key que utilizamos para gerar o token em nossa aplicação.

E por fim, vamos criar a rota no controller que será enviado o refresh token, para, se válido, retornar um novo token:

@Post('auth/refresh')
  reautenticar(@Body() body) {
    return this.authService.reautenticar(body);
  }

Se tudo estiver ocorrendo da maneira correta, ao enviar o refresh token para a rota acima, vamos obter o seguinte retorno:

Gerando tokens a partir do refresh token

Perfeito, para se aprofundar no assunto aconselho a ler o artigo Autenticação: Refresh Token com NestJS.

Nest.js - Fundamentos
Curso Nest.js - Fundamentos
Conhecer o curso

Conclusão

Neste guia abordamos vários tópicos, do básico ao avançado sobre a utilização do framework NestJS. Aprendemos a criar um projeto do zero, desenvolver um CRUD, enviar emails, realizar upload de arquivos tanto localmente quanto na AWS, aplicar validações e serialização de dados e muito mais! Lembrando que para cada tópico aqui abordado há um artigo exclusivo abordando o assunto de forma mais detalhada.

Por fim, caso queira aprender mais sobre NestJS saiba que aqui na TreinaWeb temos o curso Nest.js - Fundamentos que possui 02h07 de vídeos e um total de 18 exercícios. Conheça também nossos outros cursos de TypeScript.

Veja quais são os tópicos abordados durante o curso de Nest.js - Fundamentos:

  • Conhecendo a estrutura;
  • Utilizando Nest CLI
  • Entendendo Rotas, Controllers e Views;;
  • Conexão com banco de dados;
  • Usando TypeORM;
  • Template Engine.

Autor(a) do artigo

Wesley Gado
Wesley Gado

Formado em Análise e Desenvolvimento de Sistemas pelo Instituto Federal de São Paulo, atuou em projetos como desenvolvedor Front-End. Nas horas vagas grava Podcast e arrisca uns três acordes no violão.

Todos os artigos

Artigos relacionados Ver todos