refactor: extract bff bootstrap logic
This commit is contained in:
parent
a22b84f128
commit
01e0f41c14
152
apps/bff/src/app/bootstrap.ts
Normal file
152
apps/bff/src/app/bootstrap.ts
Normal 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;
|
||||||
|
}
|
||||||
@ -1,151 +1,45 @@
|
|||||||
import { NestFactory } from "@nestjs/core";
|
import { Logger, type INestApplication } from "@nestjs/common";
|
||||||
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 { AppModule } from "./app.module";
|
import { bootstrap } from "./app/bootstrap";
|
||||||
import { GlobalExceptionFilter } from "@bff/core/http/http-exception.filter";
|
|
||||||
|
|
||||||
async function bootstrap() {
|
const logger = new Logger("Main");
|
||||||
const app = await NestFactory.create(AppModule, {
|
let app: INestApplication | null = null;
|
||||||
bufferLogs: true,
|
|
||||||
// bodyParser is enabled by default in NestJS
|
|
||||||
rawBody: true, // Enable raw body access for debugging
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set Pino as the logger
|
const signals: NodeJS.Signals[] = ["SIGINT", "SIGTERM"];
|
||||||
app.useLogger(app.get(Logger));
|
for (const signal of signals) {
|
||||||
|
process.once(signal, async () => {
|
||||||
|
logger.log(`Received ${signal}. Closing Nest application...`);
|
||||||
|
|
||||||
const configService = app.get(ConfigService);
|
if (!app) {
|
||||||
const logger = app.get(Logger);
|
logger.warn("Nest application not initialized. Exiting immediately.");
|
||||||
|
process.exit(0);
|
||||||
// Enhanced Security Headers
|
return;
|
||||||
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
|
try {
|
||||||
const corsOrigin = configService.get<string | undefined>("CORS_ORIGIN");
|
await app.close();
|
||||||
app.enableCors({
|
logger.log("Nest application closed gracefully.");
|
||||||
origin: corsOrigin ? [corsOrigin] : false,
|
} catch (error) {
|
||||||
credentials: true,
|
const resolvedError = error as Error;
|
||||||
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
|
logger.error(
|
||||||
allowedHeaders: [
|
`Error during Nest application shutdown: ${resolvedError.message}`,
|
||||||
"Origin",
|
resolvedError.stack
|
||||||
"X-Requested-With",
|
);
|
||||||
"Content-Type",
|
} finally {
|
||||||
"Accept",
|
process.exit(0);
|
||||||
"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`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void bootstrap();
|
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
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user