Files
stack/apps/api/src/main.ts
Jason Woltje 72c64d2eeb
All checks were successful
ci/woodpecker/push/orchestrator Pipeline was successful
ci/woodpecker/push/api Pipeline was successful
ci/woodpecker/push/web Pipeline was successful
fix(api): add global /api prefix to resolve frontend route mismatch (#507)
Co-authored-by: Jason Woltje <jason@diversecanvas.com>
Co-committed-by: Jason Woltje <jason@diversecanvas.com>
2026-02-26 01:13:48 +00:00

102 lines
3.0 KiB
TypeScript

import { NestFactory } from "@nestjs/core";
import { RequestMethod, ValidationPipe } from "@nestjs/common";
import cookieParser from "cookie-parser";
import { AppModule } from "./app.module";
import { getTrustedOrigins } from "./auth/auth.config";
import { GlobalExceptionFilter } from "./filters/global-exception.filter";
function getPort(): number {
const portEnv = process.env.PORT;
if (portEnv === undefined || portEnv === "") {
return 3001;
}
const port = parseInt(portEnv, 10);
if (isNaN(port)) {
throw new Error(`Invalid PORT environment variable: "${portEnv}". PORT must be a number.`);
}
if (port < 1 || port > 65535) {
throw new Error(
`Invalid PORT environment variable: ${String(port)}. PORT must be between 1 and 65535.`
);
}
return port;
}
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Enable cookie parser for session handling
app.use(cookieParser());
// Enable global validation pipe with transformation
app.useGlobalPipes(
new ValidationPipe({
transform: true,
whitelist: true,
forbidNonWhitelisted: true,
transformOptions: {
enableImplicitConversion: false,
},
})
);
app.useGlobalFilters(new GlobalExceptionFilter());
// Set global API prefix — all routes get /api/* except auth and health
// Auth routes are excluded because BetterAuth expects /auth/* paths
// Health is excluded because Docker healthchecks hit /health directly
app.setGlobalPrefix("api", {
exclude: [
{ path: "health", method: RequestMethod.GET },
{ path: "auth/(.*)", method: RequestMethod.ALL },
],
});
// Configure CORS for cookie-based authentication
// Origin list is shared with BetterAuth trustedOrigins via getTrustedOrigins()
const trustedOrigins = getTrustedOrigins();
console.log(`[CORS] Trusted origins: ${JSON.stringify(trustedOrigins)}`);
app.enableCors({
origin: trustedOrigins,
credentials: true, // Required for cookie-based authentication
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
allowedHeaders: ["Content-Type", "Authorization", "Cookie", "X-CSRF-Token", "X-Workspace-Id"],
exposedHeaders: ["Set-Cookie"],
maxAge: 86400, // 24 hours - cache preflight requests
});
const port = getPort();
await app.listen(port);
console.log(`API running on http://localhost:${String(port)}`);
}
bootstrap().catch((err: unknown) => {
const isProduction = process.env.NODE_ENV === "production";
const errorMessage = err instanceof Error ? err.message : String(err);
const errorStack = err instanceof Error ? err.stack : undefined;
if (isProduction) {
console.error(
JSON.stringify({
level: "error",
message: "Failed to start application",
error: errorMessage,
timestamp: new Date().toISOString(),
})
);
} else {
console.error("Failed to start application:", errorMessage);
if (errorStack) {
console.error(errorStack);
}
}
process.exit(1);
});