Skip to content

Ciclo de Requisição NestJS

Toda chamada HTTP ao backend percorre um pipeline fixo e determinístico. Entender este ciclo é essencial para debugar problemas de autenticação, autorização e contexto de tenant.

Visão geral do pipeline

HTTP Request (cliente/frontend)


1. SETUP (main.ts)
   ├── Trust Proxy = 1
   ├── Helmet (headers de segurança)
   ├── rawBody capture (HMAC webhooks)
   ├── CORS dinâmico
   └── GlobalValidationPipe


2. MIDDLEWARE (executa em todo request)
   ├── CorrelationIdMiddleware   → gera/propaga x-correlation-id
   └── HttpLoggerMiddleware      → loga método/url/status/duração


3. GUARDS (execução em ordem, primeiro que falhar bloqueia)
   ├── UserThrottlerGuard        → rate limit por user ID ou IP
   ├── ClerkAuthGuard            → valida JWT do Clerk, auto-sync ao Prisma
   ├── RolesGuard                → verifica decorator @Roles
   ├── PermissionsGuard          → verifica decorator @Permissions
   └── ReadOnlyGuard             → bloqueia POST/PUT/PATCH/DELETE se role.readOnly


4. INTERCEPTORS (antes do handler)
   ├── TenantInterceptor         → popula AsyncLocalStorage com organizationId
   └── AuditInterceptor          → registra ação na tabela accessLog


5. CONTROLLER METHOD (handler)
   └── @CurrentUser() → req.user injetado


6. SERVICE LAYER
   ├── Lógica de negócio
   ├── EncryptionService (AES-256-GCM)
   ├── MailService (SMTP via Nodemailer)
   ├── MinioService (upload S3-compatible)
   └── BullMQ Queue (jobs assíncronos)


7. REPOSITORY / PRISMA
   └── PostgreSQL (com organizationId do tenant context)


8. RESPONSE
   ├── AuditInterceptor tapa a resposta → cria AccessLog
   └── x-correlation-id retornado no header

Detalhe por etapa

1. main.ts — Bootstrap

Arquivo: backend/src/main.ts

typescript
// Middlewares Express na ordem exata:
app.set('trust proxy', 1)           // IP real atrás do Traefik
app.use(helmet())                   // CSP, HSTS, etc.
app.use(json({ verify: rawBodyCapture })) // rawBody para HMAC
app.use(urlencoded({ extended: true }))
app.useGlobalPipes(new ValidationPipe({
  whitelist: true,                  // strip campos não declarados no DTO
  forbidNonWhitelisted: true,       // erro se campo extra
  transform: true,                  // cast automático de tipos
}))

CORS dinâmico: permite https://*.galdtech.com, https://galdix.com.br, e domínios listados em ALLOWED_ORIGINS.

2. Middlewares

CorrelationIdMiddleware

Gera UUID para cada request se x-correlation-id não estiver presente. Disponibiliza em req.correlationId e retorna no response header.

HttpLoggerMiddleware

Loga ao final de cada request:

GET /webhook/erp/customers 200 45ms | user=a1b2c3d4e5f6 org=org-uuid role=1 | 192.168.x.x Mozilla/5.0
  • Pseudonimização: userId/clerkId/email são hasheados (SHA-256, 12 chars hex)
  • Mascaramento de IP: IPv4 → x.x.x.*, IPv6 → primeiros 4 grupos + ****

3. Guards

Registrados globalmente no AppModule como APP_GUARD, executam na ordem registrada.

UserThrottlerGuard

  • Chave: user-{id} se autenticado, IP se anônimo
  • Limite default: 100 requisições / 60 segundos (configurável via RATE_LIMIT env)
  • Resposta: 429 Too Many Requests

ClerkAuthGuard

1. Rota marcada com @Public()? → passa
2. Rota /admin/queues? → passa (tem basic-auth próprio)
3. Extrai Authorization: Bearer <token>
4. clerkClient.verifyToken(token) → extrai sessionClaims.sub (Clerk user ID)
5. prisma.user.findUnique({ clerkId: sub })
6. Não encontrou? → chama clerkClient.users.getUser() → syncUser() → cria no DB
7. Injeta request.user = dbUser (com role, stores, organizationId)
8. Retorna true (passa)

Erros:

  • Sem token → 401 Unauthorized
  • Token inválido → 401 Unauthorized
  • Clerk retorna 403 → re-throw

RolesGuard

  • Lê metadata @Roles(roleId1, roleId2) do handler
  • role.level === 0 (Super Admin) → passa sempre
  • Verifica se user.role.id está na lista

PermissionsGuard

  • Lê metadata @Permissions('app:campaigns', ...) do handler
  • role.level === 0 → passa sempre
  • Requer que pelo menos uma das permissions esteja em user.role.permissions

ReadOnlyGuard

  • Se role.readOnly === true e método é POST/PUT/PATCH/DELETE403 Forbidden
  • role.level === 0 → passa sempre
  • Rotas @Public() → passa sempre

4. Interceptors

TenantInterceptor

typescript
// Determina organizationId ativo:
let activeOrgId = user.organizationId

// Super Admin pode override via header:
if (user.role.level === 0 && req.headers['x-org-id']) {
  activeOrgId = req.headers['x-org-id']  // deve ser UUID válido
}

// Popula AsyncLocalStorage para todo código downstream:
return tenantContext.run({ organizationId: activeOrgId, role }, () =>
  next.handle()
)

Todos os serviços podem acessar tenantContext.getStore() sem precisar receber organizationId como parâmetro.

AuditInterceptor

Registra ações em accessLog baseado em uma allowlist de regras (método + regex de rota + actionType):

RegraAction Type
POST /usersUSER_INVITE
PATCH /users/:id/roleUSER_ROLE_CHANGE
DELETE /users/:idUSER_DELETE
POST /campaignsCAMPAIGN_CREATE
POST /campaigns/:id/sendCAMPAIGN_SEND
POST /sales/trigger-syncSYNC_TRIGGERED
(e mais 15+ regras)...

Rotas que não casam com nenhuma regra são ignoradas silenciosamente.

5. Controller

typescript
@Controller('campaigns')
@Permissions('app:campaigns')
export class CampaignsController {
  @Post()
  @Permissions('app:campaigns:send')
  async create(
    @CurrentUser() user: AuthedUser,  // injetado pelo guard
    @Body() dto: CreateCampaignDto,   // validado pelo ValidationPipe
  ) {
    return this.campaignsService.create(dto, user)
  }
}

O decorator @CurrentUser() extrai req.user (populado pelo ClerkAuthGuard).

6. Tipo AuthedRequest

typescript
interface AuthedRequest extends Request {
  user: {
    id: string
    clerkId: string
    email: string
    name: string
    organizationId: string
    storeId?: string | null
    role?: {
      id: string
      name: string
      level: number          // 0 = Super Admin
      permissions: string[]
      readOnly: boolean
    } | null
    stores?: Array<{ id: string; code: string }>
  }
  rawBody?: Buffer           // para validação HMAC de webhooks
  correlationId?: string
}

7. Exemplo completo: POST /campaigns

1. Request chega: POST /campaigns
   Headers: Authorization: Bearer eyJ...

2. Trust proxy → IP real extraído
3. Helmet → headers de segurança adicionados
4. rawBody capturado (para potencial HMAC)
5. CORS validado

6. CorrelationIdMiddleware → x-correlation-id: uuid-abc123
7. HttpLoggerMiddleware → start = Date.now()

8. UserThrottlerGuard → chave "user-uuid-xyz" → 15/100 → passa
9. ClerkAuthGuard → verifica JWT → carrega user + role do DB
10. RolesGuard → nenhum @Roles → passa
11. PermissionsGuard → @Permissions('app:campaigns') → user tem → passa
12. ReadOnlyGuard → POST, user.role.readOnly=false → passa

13. TenantInterceptor → tenantContext = { organizationId: "org-123", role: {...} }
14. AuditInterceptor → inicia tap()

15. Controller: create(user, dto)
16. Service: campaignsService.create(dto, user.organizationId)
17. Prisma: INSERT INTO campaigns(...)
18. BullMQ: enfileira job de trigger (se scheduledAt)

19. Response: { id, name, status, ... }
    Status: 201 Created
    Header: x-correlation-id: uuid-abc123

20. AuditInterceptor.tap() → prisma.accessLog.create({ actionType: 'CAMPAIGN_CREATE' })
21. HttpLoggerMiddleware: "POST /campaigns 201 52ms | user=a1b2... | 192.168.x.x"

Tratamento de erros

CenárioCódigoQuem lança
Sem token401ClerkAuthGuard
Token inválido401ClerkAuthGuard
Sem permissão403PermissionsGuard
Role errada403RolesGuard
Mutação read-only403ReadOnlyGuard
DTO inválido400ValidationPipe
Rate limit429UserThrottlerGuard
Recurso não encontrado404Service (NotFoundException)
Erro interno500NestJS exception filter

Webhooks (fluxo diferente)

Rotas de webhook usam @Public() para pular o ClerkAuthGuard. A autenticação é feita por:

  • Meta WhatsApp: HMAC-SHA256 do rawBody contra APP_SECRET
  • Clerk: Svix signature (svix-id, svix-timestamp, svix-signature)
  • Chatwoot: query param ?secret=CHATWOOT_WEBHOOK_SECRET
  • n8n: Authorization: Bearer INTERNAL_API_KEY

Documentação interna — Galdix CRM