Files
sar/apps/api/src/main.ts
julian 055f9f98f0 feat(api): foundation canon JCS — Pino+CLS+RFC9457+health+ping (Frente A)
Estabelece a fundação operacional de apps/api conforme STACK.md v2.2 e
CODING-RULES.md v2.0, substituindo o hello-world scaffolded.

- tracing.ts primeiro import (PGD-OBS-001) — stub OTel ativável por env
- EnvSchema Zod 4 com fail-fast (superRefine guarda prod) + EnvModule global
- nestjs-pino com redact LGPD (*.cpf|*.cardNumber|*.password|auth|cookie)
- main.ts hardenizado: helmet, CORS por env, compression, versionamento URI
  /api/v1, graceful shutdown
- ProblemDetailsFilter global (RFC 9457 application/problem+json), Zod -> 422
- Health endpoints /api/v1/health/{live,ready} com memory.checkHeap(350MB)
  (ready skeleton documenta K=3 LRU pool conforme PGD-OBS-003)
- WorkspaceModule via ClsModule.forRootAsync — requestId+workspaceId no CLS,
  idGenerator alinhado com pino-http para mesmo UUID em header e body
- GET /api/v1/ping retornando workspaceId+requestId (alvo de smoke test e
  futuro healthcheck do docker compose)
- ZodValidationPipe (nestjs-zod) como APP_PIPE global
- tsconfig.app.json target ES2023 (alinhado ao base)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 17:50:42 +00:00

83 lines
2.6 KiB
TypeScript

// CODING-RULES §09 (PGD-OBS-001): tracing PRECISA ser o primeiro import.
// OTel NodeSDK faz monkey-patch dos módulos node:http/pg/etc. antes do Nest.
import './tracing';
import { NestFactory } from '@nestjs/core';
import { VersioningType, type INestApplication } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Logger } from 'nestjs-pino';
import helmet from 'helmet';
import compression from 'compression';
import { AppModule } from './app/app.module';
import type { Env } from './app/config/env.schema';
async function bootstrap(): Promise<void> {
const app: INestApplication = await NestFactory.create(AppModule, {
bufferLogs: true, // logs do boot vão pro Pino quando ele subir
});
// Logger Pino global (substitui o NestLogger padrão).
app.useLogger(app.get(Logger));
const config = app.get(ConfigService) as ConfigService<Env, true>;
// CORS — origens vindas do EnvSchema (já parseadas em array).
app.enableCors({
origin: config.get('CORS_ORIGINS', { infer: true }),
credentials: true,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowedHeaders: [
'Content-Type',
'Authorization',
'X-Request-Id',
'X-Correlation-Id',
'Idempotency-Key',
],
exposedHeaders: ['X-Request-Id'],
maxAge: 600,
});
// Helmet — headers de segurança canônicos.
app.use(
helmet({
contentSecurityPolicy: false, // SPA serve CSP via Nginx; API JSON dispensa.
crossOriginEmbedderPolicy: false,
}),
);
// Compression — gzip/br.
app.use(compression());
// Versionamento via URI: /api/v1/...
const globalPrefix = config.get('API_GLOBAL_PREFIX', { infer: true });
const apiVersion = config.get('API_VERSION', { infer: true }).replace(/^v/, '');
app.setGlobalPrefix(globalPrefix);
app.enableVersioning({
type: VersioningType.URI,
defaultVersion: apiVersion,
prefix: 'v',
});
// Graceful shutdown — Nest emite SIGTERM/SIGINT → hooks rodam → dreno de conexões.
// Compose: stop_grace_period: 30s + HAProxy drain 30s (CODING-RULES §18).
app.enableShutdownHooks();
const port = config.get('API_PORT', { infer: true });
const host = config.get('API_HOST', { infer: true });
await app.listen(port, host);
app
.get(Logger)
.log(
`SAR API pronta em http://${host}:${port}/${globalPrefix}/${apiVersion === '0' ? '' : `v${apiVersion}`}`,
'Bootstrap',
);
}
bootstrap().catch((err) => {
// Antes do Logger Pino estar disponível, qualquer erro de boot cai aqui.
console.error('[bootstrap] crash fatal — encerrando:', err);
process.exit(1);
});