Não deve ser confundido com Next.js, Nest.js é uma estrutura do lado do servidor que oferece uma solução completa para a construção de aplicações web. Nest é muito popular, com mais de 73.000 estrelas no GitHub no momento em que este livro foi escrito. É uma excelente escolha se você precisa construir um aplicativo do lado do servidor usando TypeScript ou JavaScript e se deseja uma solução bem pensada com todos os componentes arquitetônicos em um só lugar.

Aninhe fora da caixa

O design do Nest é filosoficamente inspirado no Angular. Em sua essência está um mecanismo de injeção de dependência (DI) que conecta todos os componentes usando um mecanismo comum. Se você conhece o Spring Web, provavelmente se sentirá em casa com o Nest.

Além de seu mecanismo DI, o Nest hospeda uma variedade de recursos integrados úteis, incluindo controladores, provedores e módulos:

  • Os controladores definem rotas HTTP e seus manipuladores.
  • Os provedores contêm a lógica de middleware usada pelos controladores (geralmente chamados de serviços em outras estruturas).
  • Os módulos agrupam controladores e provedores.

O Nest também incorpora vários recursos adicionais dignos de nota:

  • Pipes são usados ​​para validação e transformação de dados.
  • Os guardas são para autenticação e autorização.
  • Os interceptadores são um tipo de suporte AOP, utilizados para outras preocupações transversais.

A seguir, veremos como usar os componentes principais do Nest em um aplicativo do lado do servidor.

Controladores Nest: defina rotas e endpoints

Para definir uma classe de controlador no Nest, você usa o @Controller decorador:

import { Controller } from '@nestjs/common';

@Controller('birds') export class BirdsController {
// ...
} }

Este controlador existe no /birds route e nos permite definir rotas dentro dessa rota, onde cada endpoint é definido com um decorador correspondente ao método HTTP que ele manipula.

Por exemplo, se quiséssemos um GET ponto final dentro do /birds rota que aceitasse um parâmetro de tipo, poderíamos fazer isso:

@Get(':type')
findBirdsByType(@Param('type') type: string): Bird() {
  const birds = birdDatabase(type);
  if (!birds) { 
    throw new NotFoundException(`No birds found for type '${type}'.);
  }
 return birds;
}

Nest é nativo do TypeScript, então nosso birdDatabase pode ser assim:

interface Bird {
  id: number;
  name: string;
  species: string;
}

const birdDatabase: Record = {
  songbird: (
    { id: 1, name: 'Song Sparrow', species: 'Melospiza melodia' },
    { id: 2, name: 'American Robin', species: 'Turdus migratorius' },
    { id: 3, name: 'Eastern Towhee', species: 'Pipilo erythrophthalmus' },
  ),
  raptor: (
    { id: 4, name: 'Red-tailed Hawk', species: 'Buteo jamaicensis' },
    { id: 5, name: 'Peregrine Falcon', species: 'Falco peregrinus' },
    { id: 6, name: 'Bald Eagle', species: 'Haliaeetus leucocephalus' },
  ),
  corvid: (
    { id: 7, name: 'California Scrub-Jay', species: 'Aphelocoma californica' },
    { id: 8, name: 'American Crow', species: 'Corvus brachyrhynchos' },
    { id: 9, name: 'Common Raven', species: 'Corvus corax' },
  ),
};

O Nest converte automaticamente esse código em JSON apropriado, que você ajusta conforme necessário.

Provedores Nest: separe a lógica de negócios do tratamento de HTTP

Um dos princípios mais importantes na organização de aplicações web à medida que crescem em complexidade é separar as preocupações em camadas. Tanto quanto possível, queremos separar a lógica de manipulação HTTP da lógica de negócios. Para fazer isso, podemos extrair este último em uma classe de provedor (ou serviço) que é injetada no controlador.

Abaixo está um exemplo de bird provedor. O @Injectable decorador instrui o Nest a disponibilizar esta classe no mecanismo de injeção de dependência:

@Injectable()
export class BirdsService {
  private readonly birdDatabase: Record = { 
    // ... same bird data as before 
  };
  findByType(type: string): Bird() {
    const birds = this.birdDatabase(type);
    if (!birds) {
      throw new NotFoundException(`No birds found for type '${type}'.`);
    }
    return birds;
  }
}

Agora, no controlador, podemos consumir o provedor e seu findByType método assim:

import { BirdsService } from './birds.service';

@Controller('birds')
export class BirdsController {
  // Injected provider:
  constructor(private readonly birdsService: BirdsService) {}

  @Get(':type')
  findBirdsByType(@Param('type') type: string) {
    // Delegate to the provider:
    return this.birdsService.findByType(type);
  }
}

Observe que precisamos importar o BirdsService do arquivo onde está definido.

Agora temos um controlador muito simples, que trata apenas dos detalhes da própria solicitação HTTP. A lógica de negócio está toda concentrada na camada de serviço.

Módulos Nest: organize seu trabalho

Para registrar nossos controladores e provedores no motor Nest, precisamos definir um módulo que os contenha:

import { Module } from '@nestjs/common';
import { BirdsController } from './birds.controller';
import { BirdsService } from './birds.service';

@Module({
  controllers: (BirdsController),
  providers: (BirdsService),
})
export class BirdsModule {}

As classes listadas na matriz de provedores serão disponibilizadas para todos os outros provedores e controladores neste módulo, enquanto os controladores serão ativados como manipuladores.

Embora a definição do módulo adicione uma etapa extra de trabalho, é um excelente mecanismo para organizar sua aplicação. Ele permite definir áreas relacionadas do seu aplicativo e mantê-las focadas. Também limita a quantidade de código que o Nest precisa verificar para encontrar dependências.

A camada de dados Nest: persistência de dados integrada

A camada de dados é outra camada comum em uma arquitetura de aplicativo. É onde os serviços (provedores) interagem com um armazenamento de dados persistente, como um banco de dados relacional ou NoSQL.

O sistema de módulos da Nest é bastante flexível e pode suportar qualquer armazenamento de dados, mas possui módulos integrados para TypeORM, Sequelize e Mongoose, o que facilita o uso de qualquer uma dessas soluções.

Entre outras coisas (como definir informações de conexão do armazenamento de dados), um armazenamento de dados como TypeORM permite definir entidades (tipos persistentes) que armazenam os dados que entram e saem do banco de dados. Para o nosso exemplo, poderíamos ter um TypeORM entidade para aves:

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

@Entity()
export class Bird {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @Column()
  species: string;

  @Column()
  type: 'songbird' | 'raptor' | 'corvid';
}

Esta entidade pode ser usada para expor uma classe de repositório (uma classe de camada de dados). O repositório é criado pelo typeORM ferramenta quando a registramos no sistema de módulos:

//...
import { TypeOrmModule } from '@nestjs/typeorm';
import { Bird } from './bird.entity';

@Module({
  imports: (TypeOrmModule.forFeature((Bird))), // Added this
  controllers: (BirdsController),
  providers: (BirdsService),
})
export class BirdsModule {}

O TypeORMModule A função cria automaticamente várias funções CRUD com base no armazenamento de dados e na definição da entidade. Podemos então usar aqueles na camada de serviço:

import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Bird } from './bird.entity';

@Injectable()
export class BirdsService {
  constructor(
    @InjectRepository(Bird) // Bird repository is injected here
    private birdsRepository: Repository,
  ) {}

  // NOTE: DB access is async
  async findByType(type: string): Promise {
    // Actual DB call here instead of in-memory data:
    const birds = await this.birdsRepository.find({ where: { type } });

    if (!birds || birds.length === 0) {
      throw new NotFoundException(`No birds found for type '${type}'.`);
    }
    return birds;
  }
}

Objetos de transferência de dados e validação DTO

Até agora, estivemos apenas lendo dados. O outro lado da moeda é aceitar dados do usuário. Para isso, utilizamos DTOs (objetos de transferência de dados). Isso nos permite definir a forma dos dados que coletamos.

Por exemplo, se quiséssemos aceitar um novo tipo de pássaro do usuário, poderia ser algo assim:

import { IsString, IsNotEmpty, IsIn } from 'class-validator';

export class CreateBirdDto {
  @IsString()
  @IsNotEmpty()
  name: string;

  @IsString()
  @IsNotEmpty()
  species: string;
  
  // Hardcoding these values for convenience
  @IsIn(('songbird', 'raptor', 'corvid'))
  type: 'songbird' | 'raptor' | 'corvid';
}

O create-bird.dto descreve quais valores são permitidos para o processo de criação de aves. Podemos então usar isso no controlador para adicionar um endpoint de criação de pássaros:

import { CreateBirdDto } from './create-bird.dto';
//...
@Post() 
create(@Body() createBirdDto: CreateBirdDto) { 
  return this.birdsService.create(createBirdDto); 
}

Também o usamos no provedor de serviços de aves:

async create(createBirdDto: CreateBirdDto): Promise { 
// Uses TypeORM's .create() to make a new bird entity 
const newBird = this.birdsRepository.create(createBirdDto); 
  // TypeORM’s save() method persists the entity:
  return this.birdsRepository.save(newBird); 
}

Ative a validação DTO com um pipe

Agora está tudo pronto para fazer cumprir as regras definidas pelo DTO. Podemos ativar o validador DTO dizendo ao Nest para usar todos os validadores globalmente:

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

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

  // Enable auto-validation for our entire application
  app.useGlobalPipes(new ValidationPipe());

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

O embutido ValidationPipe aplicará todos os DTOs definidos no aplicativo. Agora, a própria Nest rejeitará solicitações que não atendam aos requisitos do DTO com um 400 Bad Request.

Conclusão

Este artigo foi uma visão geral da arquitetura interna central do Nest. Além de seus componentes principais, o Nest tem excelente suporte CLI, como geradores para DTOs e controladores, andaimes e modo de desenvolvimento, bem como alvos de implantação multiplataforma, como Node ou Fastify, e compilações compatíveis com Docker.

Nest é uma estrutura de servidor moderna e completa para TypeScript e JavaScript. É uma ótima opção para projetos substanciais que exigem suporte robusto do lado do servidor.