bubbles-express-generator
Version:
A simple CLI to scaffold Express.js starter projects.
149 lines (125 loc) • 4.16 kB
text/typescript
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import type { Request, Response } from 'express';
import cookieParser from 'cookie-parser';
import cors from 'cors';
import express from 'express';
import helmet from 'helmet';
import { pinoHttp } from 'pino-http';
import { env } from './config/env.js';
import { closeDatabase, pingDatabase } from './db/index.js';
import { errorHandler } from './middleware/error-handler.js';
import { notFoundHandler } from './middleware/not-found.js';
import { router as userRouter } from './routes/user.js';
/**
* Builds the CORS allowlist and enforces a fail-closed default in production.
*
* Usage: call during app bootstrap before registering the `cors` middleware.
* Expects `env.CORS_ORIGIN` as a comma-separated string; returns a normalized
* origin array with a localhost fallback for local development.
*/
const resolveAllowedOrigins = (): string[] => {
const configuredOrigins =
env.CORS_ORIGIN?.split(',')
.map((origin) => origin.trim())
.filter(Boolean) ?? [];
if (env.NODE_ENV === 'production' && configuredOrigins.length === 0) {
throw new Error('CORS_ORIGIN must be set in production');
}
if (configuredOrigins.length > 0) {
return configuredOrigins;
}
return ['http://localhost:3000'];
};
/**
* Builds the fully wired Express instance without binding a network port.
*
* Usage: call once at startup or from tests to reuse one middleware stack.
* Expects validated env values and route modules; returns an Express app ready
* for `listen()` in runtime and `supertest` in tests.
*/
export const createApp = () => {
const app = express();
const allowedOrigins = resolveAllowedOrigins();
if (env.TRUST_PROXY === '1') {
app.set('trust proxy', 1);
}
const pinoOptions =
env.NODE_ENV === 'development'
? {
transport: {
target: 'pino-pretty',
options: { colorize: true, translateTime: 'SYS:standard' },
},
}
: {};
app.use(pinoHttp(pinoOptions));
app.use(helmet());
app.use(express.json());
app.use(
cors({
origin: allowedOrigins.length === 1 ? allowedOrigins[0] : allowedOrigins,
credentials: true,
}),
);
app.use(cookieParser());
app.get('/health', (_req: Request, res: Response) => {
res.status(200).json({ status: 'ok' });
});
app.get('/ready', async (_req: Request, res: Response) => {
try {
await pingDatabase();
res.status(200).json({ status: 'ready' });
} catch {
res.status(503).json({ status: 'not_ready' });
}
});
app.get('/', (_req: Request, res: Response) => {
res.send('Hello, World!');
});
app.use('/users', userRouter);
app.use(notFoundHandler);
app.use(errorHandler);
return app;
};
export const app = createApp();
/**
* Starts the HTTP server on the configured port after startup prerequisites.
*
* Usage: invoke only in direct-run mode, not when importing for tests.
* Expects validated configuration and initialized dependencies; returns the
* startup completion signal for the current template variant.
*/
export const startServer = (): void => {
app.listen(env.PORT, () => {
console.log(`🫡 Server is running at: http://localhost:${env.PORT}`);
});
};
/**
* Handles termination signals by closing database resources before exit.
*
* Usage: bind to `SIGINT` and `SIGTERM` during direct-run startup.
* Expects the received signal label for logging and returns a promise that
* ends the process with a success or failure exit code.
*/
const shutdown = async (signal: string): Promise<void> => {
try {
console.log(`\n${signal} received. Closing database connection...`);
await closeDatabase();
process.exit(0);
} catch (error) {
console.error('Error while shutting down server:', error);
process.exit(1);
}
};
const isDirectRun =
process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
if (isDirectRun) {
startServer();
process.on('SIGINT', () => {
void shutdown('SIGINT');
});
process.on('SIGTERM', () => {
void shutdown('SIGTERM');
});
}