Preparing for Production
Before you think about where to host, the app itself has to be production-ready. The same code that's fine in development will leak stack traces, crash on the first uncaught error, ignore the other seven CPU cores, and lose in-flight requests on every deploy — unless you prepare it. This page is the production-readiness checklist that lives in your code: setting NODE_ENV, handling errors so the process survives, graceful shutdown, the security and performance baseline, health checks, and logging — the things that must be true before deployment, regardless of the hosting model you choose.
Set NODE_ENV and read config from the environment
// 1. NODE_ENV=production must be set in the environment (covered earlier):
// → faster framework code paths, no leaked stack traces.
// 2. Config comes from validated env vars — never hard-coded, fail fast if missing:
import { config } from './config' // the validated, frozen config module
const server = app.listen(config.PORT, () =>
console.log(`listening on ${config.PORT} in ${config.NODE_ENV} mode`),
)Handle errors so the process survives
// An UNHANDLED error will crash Node. In production you must catch the last-resort cases:
process.on('uncaughtException', (err) => {
logger.fatal(err, 'uncaught exception') // log it...
process.exit(1) // ...then EXIT — state may be corrupt; let the supervisor restart
})
process.on('unhandledRejection', (reason) => {
logger.error(reason, 'unhandled promise rejection')
process.exit(1) // treat like uncaughtException
})
// But these are SAFETY NETS — real handling belongs in try/catch and Express error middleware.Graceful shutdown
// On deploy/scale-down, the platform sends SIGTERM. Don't drop in-flight requests:
function shutdown(signal) {
logger.info(`${signal} received — shutting down gracefully`)
server.close(async () => { // 1. stop accepting NEW connections, finish in-flight ones
await db.end() // 2. close DB pool, drain queues, release resources
await redis.quit()
process.exit(0) // 3. exit cleanly
})
// 4. Hard cap so a stuck connection can't block shutdown forever:
setTimeout(() => process.exit(1), 10_000).unref()
}
process.on('SIGTERM', () => shutdown('SIGTERM'))
process.on('SIGINT', () => shutdown('SIGINT'))Security and performance baseline
Don't leak errors — generic messages and status codes to clients; full detail only in logs.
Rate-limit and validate — cap request rates and validate every input; never trust the client.
Drop privileges — never run as root; run as an unprivileged user, especially in containers.
Enable compression and sensible caching, and keep dependencies patched (
npm audit).Set timeouts — server, and on every outbound call, so a slow dependency can't pile up requests.
Observability — you can't fix what you can't see
// Health endpoints so load balancers / orchestrators know if the instance is usable:
app.get('/health/live', (req, res) => res.sendStatus(200)) // is the process up?
app.get('/health/ready', async (req, res) => { // can it serve traffic?
const ok = await db.ping().then(() => true).catch(() => false)
res.sendStatus(ok ? 200 : 503)
})
// Structured logging to stdout (the platform collects it) — no console.log in prod:
logger.info({ event: 'startup', port: config.PORT })