refactor: extract bff bootstrap logic

This commit is contained in:
NTumurbars 2025-09-18 15:18:12 +09:00
parent a22b84f128
commit 01e0f41c14
2 changed files with 188 additions and 142 deletions

View File

@ -0,0 +1,152 @@
import { type INestApplication, ValidationPipe } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { NestFactory } from "@nestjs/core";
import { SwaggerModule, DocumentBuilder } from "@nestjs/swagger";
import { Logger } from "nestjs-pino";
import helmet from "helmet";
import cookieParser from "cookie-parser";
import * as express from "express";
import { GlobalExceptionFilter } from "@bff/core/http/http-exception.filter";
import { AppModule } from "../app.module";
export async function bootstrap(): Promise<INestApplication> {
const app = await NestFactory.create(AppModule, {
bufferLogs: true,
// bodyParser is enabled by default in NestJS
rawBody: true, // Enable raw body access for debugging
});
// Set Pino as the logger
app.useLogger(app.get(Logger));
const configService = app.get(ConfigService);
const logger = app.get(Logger);
// Enhanced Security Headers
app.use(
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
mediaSrc: ["'self'"],
frameSrc: ["'none'"],
},
},
crossOriginEmbedderPolicy: false,
crossOriginResourcePolicy: { policy: "cross-origin" },
})
);
// Disable x-powered-by header
const expressInstance = app.getHttpAdapter().getInstance() as {
disable?: (name: string) => void;
};
if (typeof expressInstance?.disable === "function") {
expressInstance.disable("x-powered-by");
}
// Configure JSON body parser with proper limits
app.use(express.json({ limit: "10mb" }));
app.use(express.urlencoded({ extended: true, limit: "10mb" }));
// Enhanced cookie parser with security options
app.use(cookieParser());
// Trust proxy configuration for reverse proxies
if (configService.get("TRUST_PROXY", "false") === "true") {
const httpAdapter = app.getHttpAdapter();
const instance = httpAdapter.getInstance() as {
set?: (key: string, value: unknown) => void;
};
if (typeof instance?.set === "function") {
instance.set("trust proxy", 1);
}
}
// Enhanced CORS configuration
const corsOrigin = configService.get<string | undefined>("CORS_ORIGIN");
app.enableCors({
origin: corsOrigin ? [corsOrigin] : false,
credentials: true,
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
allowedHeaders: [
"Origin",
"X-Requested-With",
"Content-Type",
"Accept",
"Authorization",
"X-API-Key",
],
exposedHeaders: ["X-Total-Count", "X-Page-Count"],
maxAge: 86400, // 24 hours
});
// Global validation pipe with enhanced security
const exposeValidation = configService.get("EXPOSE_VALIDATION_ERRORS", "false") === "true";
app.useGlobalPipes(
new ValidationPipe({
transform: true,
whitelist: true,
forbidNonWhitelisted: true,
forbidUnknownValues: true,
disableErrorMessages: !exposeValidation && configService.get("NODE_ENV") === "production",
validationError: {
target: false,
value: false,
},
})
);
// Global exception filter
app.useGlobalFilters(new GlobalExceptionFilter(app.get(Logger)));
// Global authentication guard will be registered via APP_GUARD provider in AuthModule
// Rely on Nest's built-in shutdown hooks. External orchestrator will send signals.
app.enableShutdownHooks();
// Swagger documentation (only in non-production) - SETUP BEFORE GLOBAL PREFIX
if (configService.get("NODE_ENV") !== "production") {
const config = new DocumentBuilder()
.setTitle("Customer Portal API")
.setDescription("Backend for Frontend API for customer portal")
.setVersion("1.0")
.addBearerAuth()
.addCookieAuth("auth-cookie")
.addServer("http://localhost:4000", "Development server")
.addServer("https://api.yourdomain.com", "Production server")
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup("docs", app, document);
}
// API routing prefix is applied via RouterModule in AppModule for clarity and modern routing.
const port = Number(configService.get("BFF_PORT", 4000));
await app.listen(port, "0.0.0.0");
// Enhanced startup information
logger.log(`🚀 BFF API running on: http://localhost:${port}/api`);
logger.log(`🌐 Frontend Portal: http://localhost:${configService.get("NEXT_PORT", 3000)}`);
logger.log(
`🗄️ Database: ${configService.get("DATABASE_URL", "postgresql://dev:dev@localhost:5432/portal_dev")}`
);
logger.log(`🔗 Prisma Studio: http://localhost:5555`);
logger.log(`🔴 Redis: ${configService.get("REDIS_URL", "redis://localhost:6379")}`);
if (configService.get("NODE_ENV") !== "production") {
logger.log(`📚 API Documentation: http://localhost:${port}/docs`);
}
return app;
}

View File

@ -1,151 +1,45 @@
import { NestFactory } from "@nestjs/core";
import { ValidationPipe } from "@nestjs/common";
import { SwaggerModule, DocumentBuilder } from "@nestjs/swagger";
import { ConfigService } from "@nestjs/config";
import { Logger } from "nestjs-pino";
import helmet from "helmet";
import cookieParser from "cookie-parser";
import * as express from "express";
import { Logger, type INestApplication } from "@nestjs/common";
import { AppModule } from "./app.module";
import { GlobalExceptionFilter } from "@bff/core/http/http-exception.filter";
import { bootstrap } from "./app/bootstrap";
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
bufferLogs: true,
// bodyParser is enabled by default in NestJS
rawBody: true, // Enable raw body access for debugging
const logger = new Logger("Main");
let app: INestApplication | null = null;
const signals: NodeJS.Signals[] = ["SIGINT", "SIGTERM"];
for (const signal of signals) {
process.once(signal, async () => {
logger.log(`Received ${signal}. Closing Nest application...`);
if (!app) {
logger.warn("Nest application not initialized. Exiting immediately.");
process.exit(0);
return;
}
try {
await app.close();
logger.log("Nest application closed gracefully.");
} catch (error) {
const resolvedError = error as Error;
logger.error(
`Error during Nest application shutdown: ${resolvedError.message}`,
resolvedError.stack
);
} finally {
process.exit(0);
}
});
}
// Set Pino as the logger
app.useLogger(app.get(Logger));
const configService = app.get(ConfigService);
const logger = app.get(Logger);
// Enhanced Security Headers
app.use(
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
mediaSrc: ["'self'"],
frameSrc: ["'none'"],
},
},
crossOriginEmbedderPolicy: false,
crossOriginResourcePolicy: { policy: "cross-origin" },
void bootstrap()
.then((startedApp) => {
app = startedApp;
})
.catch((error) => {
const resolvedError = error as Error;
logger.error(
`Failed to bootstrap the Nest application: ${resolvedError.message}`,
resolvedError.stack
);
// Disable x-powered-by header
const expressInstance = app.getHttpAdapter().getInstance() as {
disable?: (name: string) => void;
};
if (typeof expressInstance?.disable === "function") {
expressInstance.disable("x-powered-by");
}
// Configure JSON body parser with proper limits
app.use(express.json({ limit: "10mb" }));
app.use(express.urlencoded({ extended: true, limit: "10mb" }));
// Enhanced cookie parser with security options
app.use(cookieParser());
// Trust proxy configuration for reverse proxies
if (configService.get("TRUST_PROXY", "false") === "true") {
const httpAdapter = app.getHttpAdapter();
const instance = httpAdapter.getInstance() as {
set?: (key: string, value: unknown) => void;
};
if (typeof instance?.set === "function") {
instance.set("trust proxy", 1);
}
}
// Enhanced CORS configuration
const corsOrigin = configService.get<string | undefined>("CORS_ORIGIN");
app.enableCors({
origin: corsOrigin ? [corsOrigin] : false,
credentials: true,
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
allowedHeaders: [
"Origin",
"X-Requested-With",
"Content-Type",
"Accept",
"Authorization",
"X-API-Key",
],
exposedHeaders: ["X-Total-Count", "X-Page-Count"],
maxAge: 86400, // 24 hours
process.exit(1);
});
// Global validation pipe with enhanced security
const exposeValidation = configService.get("EXPOSE_VALIDATION_ERRORS", "false") === "true";
app.useGlobalPipes(
new ValidationPipe({
transform: true,
whitelist: true,
forbidNonWhitelisted: true,
forbidUnknownValues: true,
disableErrorMessages: !exposeValidation && configService.get("NODE_ENV") === "production",
validationError: {
target: false,
value: false,
},
})
);
// Global exception filter
app.useGlobalFilters(new GlobalExceptionFilter(app.get(Logger)));
// Global authentication guard will be registered via APP_GUARD provider in AuthModule
// Rely on Nest's built-in shutdown hooks. External orchestrator will send signals.
app.enableShutdownHooks();
// Swagger documentation (only in non-production) - SETUP BEFORE GLOBAL PREFIX
if (configService.get("NODE_ENV") !== "production") {
const config = new DocumentBuilder()
.setTitle("Customer Portal API")
.setDescription("Backend for Frontend API for customer portal")
.setVersion("1.0")
.addBearerAuth()
.addCookieAuth("auth-cookie")
.addServer("http://localhost:4000", "Development server")
.addServer("https://api.yourdomain.com", "Production server")
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup("docs", app, document);
}
// API routing prefix is applied via RouterModule in AppModule for clarity and modern routing.
const port = Number(configService.get("BFF_PORT", 4000));
await app.listen(port, "0.0.0.0");
// Enhanced startup information
logger.log(`🚀 BFF API running on: http://localhost:${port}/api`);
logger.log(`🌐 Frontend Portal: http://localhost:${configService.get("NEXT_PORT", 3000)}`);
logger.log(
`🗄️ Database: ${configService.get("DATABASE_URL", "postgresql://dev:dev@localhost:5432/portal_dev")}`
);
logger.log(`🔗 Prisma Studio: http://localhost:5555`);
logger.log(`🔴 Redis: ${configService.get("REDIS_URL", "redis://localhost:6379")}`);
if (configService.get("NODE_ENV") !== "production") {
logger.log(`📚 API Documentation: http://localhost:${port}/docs`);
}
}
void bootstrap();