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>
83 lines
2.6 KiB
TypeScript
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);
|
|
});
|