Skip to content
← All posts
054 min read

Node.js + TypeScript Production Checklist: 12 Things Before You Go Live

  • devops
  • performance
  • typescript
Node.js + TypeScript Production Checklist: 12 Things Before You Go Live

Most Node.js apps skip at least 5 of these. Check them before your production launch, not after.

PUBLISH METADATA

  • Tags: Node.js, TypeScript, DevOps, Software Engineering, Web Development
  • Description: A 12-point production readiness checklist for Node.js + TypeScript applications — covering security, performance, monitoring, error handling, and deployment best practices for 2026.
  • Suggested publish date: Week 5 — standalone checklist format for sharing/bookmarking
  • Suggested cover image: Checklist graphic with 12 items. Green checkmarks. Node.js + TypeScript logos. Clean minimal design.

Introduction

I’ve launched a dozen Node.js services to production. Every time I skip something on this list, I regret it within 2 weeks.

This is the checklist I now run through before every production launch. 12 items. All of them matter.

#1 — Structured Logging (Not console.log)

// ❌ Never in production
console.log('User logged in:', userId)
// ✅ Structured JSON logging
import pino from 'pino'
const logger = pino({ level: process.env.LOG_LEVEL || 'info' })
logger.info({ userId, event: 'login' }, 'User logged in')

Why: console.log in production means unsearchable, unstructured logs. Pino outputs JSON that CloudWatch, Datadog, and any log aggregator can query.

#2 — Environment Variable Validation at Startup

// config/env.ts
import { z } from 'zod'
const EnvSchema = z.object({
NODE_ENV: z.enum(['development', 'test', 'production']),
PORT: z.string().regex(/^\d+$/).transform(Number),
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32),
REDIS_URL: z.string().url().optional(),
})
export const env = (() => {
const result = EnvSchema.safeParse(process.env)
if (!result.success) {
console.error('Missing environment variables:', result.error.flatten())
process.exit(1)
}
return result.data
})()

Why: Your app should crash immediately at startup with a clear error if a required env var is missing — not 2 hours later when that code path is hit.

#3 — Graceful Shutdown

Graceful Shutdown
Graceful Shutdown
// server.ts
const server = app.listen(env.PORT)
async function gracefulShutdown(signal: string) {
logger.info({ signal }, 'Received shutdown signal')
  server.close(async () => {
logger.info('HTTP server closed')
await db.$disconnect()
await redis.quit()
logger.info('Database connections closed')
process.exit(0)
})
  // Force exit if graceful shutdown takes too long
setTimeout(() => {
logger.error('Graceful shutdown timeout — forcing exit')
process.exit(1)
}, 10000)
}
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'))
process.on('SIGINT', () => gracefulShutdown('SIGINT'))

Why: Without this, ECS/Kubernetes task termination will kill in-flight requests. Graceful shutdown drains them first.

#4 — Global Error Handler + Unhandled Promise Rejection Handler

// Global Express error handler
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
logger.error({ err, path: req.path, method: req.method }, 'Unhandled error')
res.status(500).json({ error: 'Internal server error' })
})
// Catch unhandled promise rejections
process.on('unhandledRejection', (reason) => {
logger.fatal({ reason }, 'Unhandled promise rejection — shutting down')
process.exit(1)
})
process.on('uncaughtException', (err) => {
logger.fatal({ err }, 'Uncaught exception — shutting down')
process.exit(1)
})

#5 — Rate Limiting

import rateLimit from 'express-rate-limit'
import RedisStore from 'rate-limit-redis'
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100,
standardHeaders: true,
legacyHeaders: false,
store: new RedisStore({ client: redis }), // Distributed across containers
message: { error: 'Too many requests — try again in 15 minutes' },
})
app.use('/api/', limiter)
// Stricter limit for auth endpoints
const authLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 5 })
app.use('/api/auth/', authLimiter)

#6 — Security Headers with Helmet

import helmet from 'helmet'
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", 'data:', 'https:'],
}
},
hsts: { maxAge: 31536000, includeSubDomains: true },
}))

#7 — Request Timeout

import timeout from 'connect-timeout'
app.use(timeout('30s'))
app.use((req, res, next) => {
if (req.timedout) return // Already handled
next()
})

Why: Without timeouts, slow database queries can hold connections open indefinitely, eventually exhausting your connection pool.

#8 — Health Check Endpoint

app.get('/health', async (req, res) => {
try {
await db.$queryRaw`SELECT 1`
res.json({ status: 'healthy', uptime: process.uptime() })
} catch {
res.status(503).json({ status: 'unhealthy' })
}
})

#9 — Compression

import compression from 'compression'
app.use(compression())

Why: 60–80% reduction in response size for JSON APIs. Free performance gain.

#10 — Request ID Correlation

Request ID Correlation
Request ID Correlation
import { v4 as uuid } from 'uuid'
app.use((req, res, next) => {
const requestId = req.headers['x-request-id'] as string || uuid()
res.setHeader('x-request-id', requestId)
req.requestId = requestId
next()
})
// Include requestId in every log
logger.info({ requestId: req.requestId, path: req.path }, 'Request received')

Why: When a customer reports an error, you can find their exact request chain in logs instantly.

#11 — Database Query Monitoring

// Prisma query logging
const db = new PrismaClient({
log: [
{ emit: 'event', level: 'query' },
{ emit: 'event', level: 'warn' },
{ emit: 'event', level: 'error' },
],
})
db.$on('query', (e) => {
if (e.duration > 1000) { // Log slow queries > 1 second
logger.warn({ query: e.query, duration: e.duration }, 'Slow query detected')
}
})

#12 — Memory Leak Detection

// Log memory usage every 5 minutes
setInterval(() => {
const { heapUsed, heapTotal, rss } = process.memoryUsage()
logger.info({
heapUsedMB: Math.round(heapUsed / 1024 / 1024),
heapTotalMB: Math.round(heapTotal / 1024 / 1024),
rssMB: Math.round(rss / 1024 / 1024),
}, 'Memory usage')
}, 5 * 60 * 1000)

Set a CloudWatch alarm if your ECS task memory usage exceeds 80% consistently — that’s usually a memory leak.

The Quick Summary

Node.js + TypeScript Production Checklist: 12 Things Before You Go Live

#ItemCritical?1Structured logging (Pino)✅ Yes2Env var validation at startup✅ Yes3Graceful shutdown✅ Yes4Unhandled rejection handler✅ Yes5Rate limiting✅ Yes6Security headers (Helmet)✅ Yes7Request timeout✅ Yes8Health check endpoint✅ Yes9Compression⚡ Performance10Request ID correlation🔍 Observability11Database query monitoring🔍 Observability12Memory usage logging🔍 Observability

If you check all 12 before launch, you’re in a much better position than 90% of Node.js apps hitting production.

#Nodejs #TypeScript #DevOps #WebDevelopment #SoftwareEngineering #Production #Backend #Security #Performance

I’m a Full-Stack Developer specializing in React, Next.js, and Node.js.
👉 Connect with me: https://www.linkedin.com/in/chiragmehta900/

Author card for Chirag Mehta, Full-Stack Developer specializing in React, Next.js and Node.js. Links: github.com/chiragmehta900, linkedin.com/in/chiragmehta900, medium.com/@chiragmehta900.
Chirag Mehta

Originally published on Medium

Clap, comment or follow along there

Read on Medium →