NestJS 11 Enterprise API Patterns

Master NestJS 11 enterprise patterns: guards, interceptors, pipes, multi-tenancy, and production-ready API design for scalable backend systems.

E
ECOSIRE Research and Development Team
|19 de março de 202613 min de leitura2.9k Palavras|

Padrões de API empresarial NestJS 11

NestJS 11 emergiu como a estrutura ideal para a construção de APIs de nível de produção em TypeScript, combinando a arquitetura de inspiração Angular com o poder bruto do Node.js. Quando você está operando em escala empresarial — lidando com milhões de solicitações, gerenciando dados de vários locatários e coordenando dezenas de módulos — os padrões que você escolhe no primeiro dia determinam se sua base de código é dimensionada normalmente ou colapsada sob seu próprio peso.

Este guia destila lições duramente adquiridas na construção de um back-end NestJS 11 de 56 módulos com mais de 310 arquivos TypeScript, cobrindo tudo, desde organização de módulos e composição de proteção até padrões de multilocação que realmente resistem sob carga.

Principais conclusões

  • Use forRoutes('*path') e não forRoutes('*') — NestJS 11 alterou a correspondência de rota curinga
  • Os filtros de exceção globais devem ser registrados em main.ts, não através de APP_FILTER
  • A multilocação requer filtragem organizationId em todas as camadas de consulta, não apenas no middleware
  • O padrão decorador @Public() é mais seguro do que desabilitar totalmente os guardas para rotas abertas
  • Nunca use sql.raw() em consultas Drizzle — sempre literais de modelo sql parametrizados
  • EmailModule deve ser @Global() para evitar a reimportação em todos os módulos de recursos
  • Dotenv deve ser pré-carregado em main.ts antes da inicialização do NestJS para evitar problemas de resolução de ambiente
  • A limitação de taxa é obrigatória em todos os endpoints públicos — use @nestjs/throttler

Estrutura do projeto em escala

A decisão arquitetônica mais importante no NestJS é como você organiza os módulos. Em escala empresarial, uma lista simples de módulos em app.module.ts torna-se incontrolável. O padrão que funciona é o agrupamento de módulos controlado por domínio com declarações de dependência explícitas.

apps/api/src/
  modules/
    auth/
      auth.module.ts
      auth.controller.ts
      auth.service.ts
      guards/
        jwt.guard.ts
        roles.guard.ts
      decorators/
        public.decorator.ts
        roles.decorator.ts
    contacts/
      contacts.module.ts
      contacts.controller.ts
      contacts.service.ts
      contacts.spec.ts
      dto/
        create-contact.dto.ts
        update-contact.dto.ts
    billing/
      billing.module.ts
      billing.service.ts
      webhook.controller.ts
  shared/
    filters/
      global-exception.filter.ts
    interceptors/
      transform.interceptor.ts
    pipes/
      validation.pipe.ts
  health/
    health.controller.ts
    indicators/
  main.ts
  app.module.ts

Cada módulo de domínio é independente. O diretório shared/ contém preocupações transversais. Essa separação permite adicionar novos domínios sem alterar o código existente.


O padrão decorador @Public()

O NestJS 11 aplica proteções JWT globalmente, mas você precisa de endpoints selecionados abertos – verificações de integridade, retornos de chamada de autenticação, receptores de webhook. O padrão decorador @Public() é muito superior à desativação dos guardas por rota.

// decorators/public.decorator.ts
import { SetMetadata } from '@nestjs/common';

export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
// guards/jwt-auth.guard.ts
import { Injectable, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { IS_PUBLIC_KEY } from '../decorators/public.decorator';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  constructor(private reflector: Reflector) {
    super();
  }

  canActivate(context: ExecutionContext) {
    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);

    if (isPublic) return true;
    return super.canActivate(context);
  }
}
// app.module.ts
import { APP_GUARD } from '@nestjs/core';

@Module({
  providers: [
    { provide: APP_GUARD, useClass: JwtAuthGuard },
    { provide: APP_GUARD, useClass: RolesGuard },
  ],
})
export class AppModule {}

Agora, qualquer método de controlador decorado com @Public() ignora totalmente a validação do JWT. Controladores de webhook, endpoints de integridade e rotas de autenticação usam esse padrão.


Multilocação com OrganizationId

A regra fundamental das APIs multilocatários: toda consulta ao banco de dados deve ser filtrada por organizationId. O middleware que define req.organizationId não é suficiente — um desenvolvedor que se esquece de aplicar o filtro expõe dados entre locatários.

O padrão é extrair organizationId da carga JWT e anexá-lo a uma interface de solicitação digitada:

// types/authenticated-request.interface.ts
import { Request } from 'express';

export interface AuthenticatedRequest extends Request {
  user: {
    sub: string;
    email: string;
    name: string;
    role: 'admin' | 'support' | 'user';
    organizationId: string;
  };
}
// contacts/contacts.controller.ts
import { Controller, Get, Post, Body, Req } from '@nestjs/common';
import { AuthenticatedRequest } from '../../types/authenticated-request.interface';

@Controller('contacts')
export class ContactsController {
  constructor(private contactsService: ContactsService) {}

  @Get()
  findAll(@Req() req: AuthenticatedRequest) {
    return this.contactsService.findAll(req.user.organizationId);
  }

  @Post()
  create(@Body() dto: CreateContactDto, @Req() req: AuthenticatedRequest) {
    return this.contactsService.create(dto, req.user.organizationId);
  }
}
// contacts/contacts.service.ts
import { db } from '@ecosire/db';
import { contacts } from '@ecosire/db/schema';
import { eq, and } from 'drizzle-orm';

@Injectable()
export class ContactsService {
  async findAll(organizationId: string) {
    return db
      .select()
      .from(contacts)
      .where(eq(contacts.organizationId, organizationId))
      .limit(100);
  }
}

A camada de serviço impõe organizationId no nível da consulta. Nenhuma falha de middleware ou decorador esquecido pode vazar dados entre locatários porque o próprio SQL impõe isolamento.


Filtro de exceção global

Respostas de erro consistentes em todos os endpoints exigem um filtro de exceção global. A hierarquia HttpException do NestJS 11 lida com a maioria dos casos, mas você também precisa detectar erros inesperados e nunca expor rastreamentos de pilha na produção.

// filters/global-exception.filter.ts
import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
  HttpStatus,
  Logger,
} from '@nestjs/common';
import { Request, Response } from 'express';

@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
  private readonly logger = new Logger(GlobalExceptionFilter.name);

  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();

    let status = HttpStatus.INTERNAL_SERVER_ERROR;
    let message = 'Internal server error';
    let errors: Record<string, string[]> | undefined;

    if (exception instanceof HttpException) {
      status = exception.getStatus();
      const exceptionResponse = exception.getResponse();

      if (typeof exceptionResponse === 'string') {
        message = exceptionResponse;
      } else if (typeof exceptionResponse === 'object') {
        const resp = exceptionResponse as Record<string, unknown>;
        message = (resp.message as string) || message;
        if (Array.isArray(resp.message)) {
          // Validation errors from class-validator
          errors = this.formatValidationErrors(resp.message as string[]);
          message = 'Validation failed';
        }
      }
    } else if (exception instanceof Error) {
      this.logger.error(exception.message, exception.stack);
      // Never expose stack traces in production
      if (process.env.NODE_ENV !== 'production') {
        message = exception.message;
      }
    }

    response.status(status).json({
      statusCode: status,
      message,
      errors,
      path: request.url,
      timestamp: new Date().toISOString(),
    });
  }

  private formatValidationErrors(messages: string[]): Record<string, string[]> {
    const errors: Record<string, string[]> = {};
    for (const msg of messages) {
      const [field, ...rest] = msg.split(' ');
      if (!errors[field]) errors[field] = [];
      errors[field].push(rest.join(' '));
    }
    return errors;
  }
}

Registre-o em main.ts:

// main.ts
import { NestFactory } from '@nestjs/core';
import { GlobalExceptionFilter } from './shared/filters/global-exception.filter';

async function bootstrap() {
  // CRITICAL: Load env vars before NestJS bootstraps
  require('dotenv').config({ path: join(__dirname, '..', '..', '..', '.env.local') });

  const app = await NestFactory.create(AppModule);
  app.useGlobalFilters(new GlobalExceptionFilter());
  app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
  await app.listen(3001);
}

A mudança do curinga da rota NestJS 11

Uma das mudanças mais importantes no NestJS 11 é a correspondência de rota curinga. Se você estiver migrando do NestJS 10, isso interromperá silenciosamente seu middleware:

// NestJS 10 — works
consumer.apply(LoggerMiddleware).forRoutes('*');

// NestJS 11 — use '*path' instead
consumer.apply(LoggerMiddleware).forRoutes('*path');

O mesmo se aplica à configuração do Swagger e a quaisquer padrões de rota baseados em strings. Essa alteração afeta o registro de middleware, exclusões de rotas e qualquer local onde você usa correspondência de rotas no estilo glob.


Controle de acesso baseado em funções

Além da autenticação JWT, as APIs corporativas precisam de autorização baseada em funções. O padrão decorador mais guarda mantém os controladores limpos:

// decorators/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';

export type Role = 'admin' | 'support' | 'user';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);
// guards/roles.guard.ts
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY, Role } from '../decorators/roles.decorator';
import { AuthenticatedRequest } from '../../types/authenticated-request.interface';

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);

    if (!requiredRoles) return true;

    const { user } = context.switchToHttp().getRequest<AuthenticatedRequest>();
    const hasRole = requiredRoles.some((role) => user.role === role);

    if (!hasRole) {
      throw new ForbiddenException('Insufficient permissions');
    }

    return true;
  }
}

Uso em controladores:

// For top-level module controllers — one `..`
import { Roles } from '../auth/guards/roles.guard';

// For nested sub-module controllers — two `../..`
import { Roles } from '../../auth/guards/roles.guard';

@Controller('admin/contacts')
@Roles('admin', 'support')
export class AdminContactsController {}

A profundidade do caminho de importação é uma fonte comum de bugs. Os controladores de módulo de nível superior usam um ..; controladores de submódulos aninhados usam dois ../...


Padrão Global EmailModule

O decorador @Global() resolve o problema de importação repetitiva para serviços usados ​​em qualquer lugar. Email é o caso de uso canônico — você não deseja importar EmailModule em cada módulo de recurso que envia notificações.

// email/email.module.ts
import { Global, Module } from '@nestjs/common';
import { EmailService } from './email.service';

@Global()
@Module({
  providers: [EmailService],
  exports: [EmailService],
})
export class EmailModule {}

Registre-o uma vez em AppModule:

@Module({
  imports: [
    EmailModule, // Global — available everywhere
    ContactsModule,
    BillingModule,
    // ...
  ],
})
export class AppModule {}

Agora qualquer serviço pode injetar EmailService sem tocar nas importações de módulos. O mesmo padrão funciona para Redis, EventBus e qualquer serviço de infraestrutura transversal.


Taxa de limitação de endpoints públicos

As APIs corporativas enfrentam tentativas de abuso diariamente. O @nestjs/throttler do NestJS integra-se perfeitamente ao sistema de proteção:

// app.module.ts
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';

@Module({
  imports: [
    ThrottlerModule.forRoot([
      { name: 'short', ttl: 1000, limit: 10 },
      { name: 'medium', ttl: 60000, limit: 100 },
    ]),
  ],
  providers: [
    { provide: APP_GUARD, useClass: ThrottlerGuard },
  ],
})

Substitua por endpoint por @Throttle() para limites mais rígidos em rotas confidenciais:

import { Throttle } from '@nestjs/throttler';

@Controller('auth')
export class AuthController {
  @Post('exchange')
  @Public()
  @Throttle({ short: { ttl: 60000, limit: 5 } }) // 5 per minute
  async exchangeCode(@Body() dto: ExchangeCodeDto) {
    return this.authService.exchangeCode(dto.code);
  }
}

Verificações de integridade com @nestjs/terminus

As APIs de produção precisam de pontos de extremidade de integridade para verificações e monitoramento do balanceador de carga. @nestjs/terminus fornece um sistema declarativo com indicadores integrados:

// health/health.controller.ts
import { Controller, Get } from '@nestjs/common';
import { HealthCheck, HealthCheckService, TypeOrmHealthIndicator } from '@nestjs/terminus';
import { Public } from '../auth/decorators/public.decorator';
import { DatabaseHealthIndicator } from './indicators/database.indicator';
import { RedisHealthIndicator } from './indicators/redis.indicator';

@Controller('health')
export class HealthController {
  constructor(
    private health: HealthCheckService,
    private db: DatabaseHealthIndicator,
    private redis: RedisHealthIndicator,
  ) {}

  @Get()
  @Public()
  @HealthCheck()
  check() {
    return this.health.check([
      () => this.db.isHealthy('database'),
      () => this.redis.isHealthy('redis'),
    ]);
  }
}

Indicador personalizado para Drizzle ORM (já que o terminal não possui um integrado):

// health/indicators/database.indicator.ts
import { Injectable } from '@nestjs/common';
import { HealthIndicator, HealthIndicatorResult, HealthCheckError } from '@nestjs/terminus';
import { db } from '@ecosire/db';
import { sql } from 'drizzle-orm';

@Injectable()
export class DatabaseHealthIndicator extends HealthIndicator {
  async isHealthy(key: string): Promise<HealthIndicatorResult> {
    try {
      await db.execute(sql`SELECT 1`);
      return this.getStatus(key, true);
    } catch (error) {
      throw new HealthCheckError('Database check failed', this.getStatus(key, false));
    }
  }
}

Padrões de validação DTO

O ValidationPipe com whitelist: true remove propriedades desconhecidas antes que elas cheguem à sua camada de serviço. Combinados com class-transformer, os DTOs se tornam sua primeira linha de defesa:

// contacts/dto/create-contact.dto.ts
import { IsString, IsEmail, IsOptional, IsEnum, MinLength, MaxLength } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Transform } from 'class-transformer';

export enum ContactType {
  INDIVIDUAL = 'individual',
  COMPANY = 'company',
}

export class CreateContactDto {
  @ApiProperty({ example: 'Acme Corp' })
  @IsString()
  @MinLength(2)
  @MaxLength(255)
  @Transform(({ value }) => value?.trim())
  name: string;

  @ApiProperty({ example: '[email protected]' })
  @IsEmail()
  @Transform(({ value }) => value?.toLowerCase().trim())
  email: string;

  @ApiPropertyOptional({ enum: ContactType })
  @IsOptional()
  @IsEnum(ContactType)
  type?: ContactType;

  @ApiPropertyOptional()
  @IsOptional()
  @IsString()
  @MaxLength(500)
  notes?: string;
}

Os decoradores @Transform normalizam os dados na camada de validação. Cortar espaços em branco e letras minúsculas em e-mails evita registros duplicados devido a diferenças de maiúsculas e minúsculas.


Integração Swagger

APIs empresariais precisam de documentação. O módulo Swagger do NestJS gera especificações OpenAPI dos decoradores, mas a configuração deve ser deliberada:

// main.ts
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';

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

  const config = new DocumentBuilder()
    .setTitle('ECOSIRE API')
    .setDescription('Enterprise API for ECOSIRE platform')
    .setVersion('1.0')
    .addBearerAuth()
    .addTag('auth', 'Authentication endpoints')
    .addTag('contacts', 'Contact management')
    .build();

  const document = SwaggerModule.createDocument(app, config);

  // Only expose in non-production environments
  if (process.env.NODE_ENV !== 'production') {
    SwaggerModule.setup('api/docs', app, document);
  }

  await app.listen(3001);
}

Todo método de controlador precisa de @ApiOperation e @ApiResponse:

@Get(':id')
@ApiOperation({ summary: 'Get contact by ID' })
@ApiResponse({ status: 200, description: 'Contact found', type: ContactResponseDto })
@ApiResponse({ status: 404, description: 'Contact not found' })
async findOne(@Param('id') id: string, @Req() req: AuthenticatedRequest) {
  return this.contactsService.findOne(id, req.user.organizationId);
}

Armadilhas e soluções comuns

Armadilha 1: Dependências circulares entre módulos

As importações circulares travam o NestJS na inicialização com um erro enigmático. A correção é forwardRef():

@Module({
  imports: [forwardRef(() => BillingModule)],
})
export class LicenseModule {}

Melhor solução: extrair a lógica compartilhada para um terceiro módulo que importa sem dependência circular.

Armadilha 2: Env vars não disponíveis durante a inicialização do módulo

Se você acessar process.env.DATABASE_URL em um construtor de classe ou fábrica de provedores antes do carregamento do dotenv, você obterá undefined. Solução: carregue dotenv no topo de main.ts, antes de qualquer importação do NestJS.

// main.ts — must be FIRST
import * as path from 'path';
require('dotenv').config({ path: path.join(__dirname, '..', '..', '..', '.env.local') });

// Then NestJS imports
import { NestFactory } from '@nestjs/core';

Armadilha 3: falta de espera em operações assíncronas em ganchos de ciclo de vida

// Wrong — database connection might not be ready
@Injectable()
export class AppService implements OnModuleInit {
  onModuleInit() {
    this.seedDatabase(); // Not awaited!
  }
}

// Correct
async onModuleInit() {
  await this.seedDatabase();
}

Erro 4: usar sql.raw() em consultas do Drizzle

sql.raw() ignora a parametrização e abre vetores de injeção SQL. Sempre use o literal do modelo sql:

// Dangerous — never do this
const result = await db.execute(sql.raw(`SELECT * FROM contacts WHERE id = '${id}'`));

// Safe — parameterized
const result = await db.execute(sql`SELECT * FROM contacts WHERE id = ${id}`);

Perguntas frequentes

Como lidar com o controle de versão nas APIs NestJS 11?

O NestJS 11 oferece suporte imediato ao versionamento de URI, versionamento de cabeçalho e versionamento de tipo de mídia. Habilite-o em main.ts com app.enableVersioning({ type: VersioningType.URI }) e decore os controladores com @Version('1'). Para APIs corporativas, o versionamento de URI (/v1/contacts) é a abordagem mais explícita e amigável ao cache.

Qual é a melhor maneira de lidar com uploads de arquivos no NestJS?

Use @nestjs/platform-express com multer para uploads de arquivos. Para uploads S3, configure um mecanismo de armazenamento personalizado por meio de multer-s3. Sempre valide os tipos e tamanhos de arquivos no nível do pipe antes da execução do manipulador e nunca confie nos tipos MIME fornecidos pelo cliente - em vez disso, valide os bytes mágicos.

Como devo estruturar transações de banco de dados no NestJS?

Drizzle ORM oferece suporte a transações com db.transaction(async (tx) => { ... }). Passe o objeto de transação para métodos de serviço em vez da instância global db. Para lógica de negócios multioperação (criar pedido + deduzir estoque + enviar e-mail), envolva tudo em uma transação e torne o envio de e-mail sem bloqueio com .catch() após a confirmação.

Quando devo usar guardas versus middleware versus interceptores?

Os guardas lidam com a autorização (este usuário pode acessar este recurso?). O middleware lida com a transformação de solicitações transversais (registro, IDs de correlação, análise). Os interceptores envolvem o ciclo de solicitação-resposta para transformação, armazenamento em cache e métricas. A ordem de execução é: Middleware → Guardas → Interceptores (antes) → Pipes → Manipulador → Interceptores (depois) → Filtros de Exceção.

Como faço para testar os módulos NestJS isoladamente?

Use Test.createTestingModule() para criar uma sandbox de teste com dependências simuladas. Simule seus métodos de serviço com jest.fn() ou vi.fn() e teste o comportamento do controlador independentemente do seu banco de dados. Para testes de integração, use @nestjs/testing com uma conexão de banco de dados real (banco de dados de teste separado) e reversão de transação após cada teste.

Qual ​​é o impacto no desempenho dos guardas globais?

Os guardas globais atendem a cada solicitação, portanto, mantenha-os rápidos. A verificação do JWT normalmente leva de 1 a 5 ms. Evite pesquisas de banco de dados em guardas – carregue permissões durante a criação do token e inclua-as na carga útil do JWT. Se você precisar de novas permissões em cada solicitação, use o Redis com um TTL curto em vez de acessar o banco de dados.


Próximas etapas

Construir APIs empresariais em escala requer a arquitetura certa desde o primeiro dia. A equipe de engenharia da ECOSIRE construiu e operou um back-end NestJS 11 de 56 módulos que lida com fluxos de trabalho complexos de vários locatários, faturamento Stripe, gerenciamento de licenças e análises baseadas em IA.

Se você precisa de uma API NestJS personalizada, integração Odoo ERP ou uma plataforma empresarial full-stack, nossa equipe traz padrões comprovados de produção para o seu projeto. Explore nossos serviços de desenvolvimento para ver como podemos acelerar sua próxima construção.

Compartilhar:
E

Escrito por

ECOSIRE Research and Development Team

Construindo produtos digitais de nível empresarial na ECOSIRE. Compartilhando insights sobre integrações Odoo, automação de e-commerce e soluções de negócios com IA.

Converse no WhatsApp