Difficulty: Intermediate
Building production-quality Node.js applications requires following established best practices that cover code organization, security, performance, reliability, and maintainability. These practices are the result of years of community experience and hard-learned lessons from production failures. Following them from the start prevents technical debt and makes your application easier to scale, debug, and maintain.
Project structure is the foundation of a maintainable application. The most effective pattern for Express applications is feature-based organization (also called modular architecture), where each feature or domain gets its own directory with routes, controllers, services, and models. This is far better than the alternative 'technical' organization (all controllers in one folder, all models in another) because related code lives together, making it easy to understand and modify a feature without jumping between distant directories.
Security is not optional - it must be built into every layer of your application. The essential security checklist includes: use HTTPS everywhere, set security headers with Helmet, configure CORS strictly, rate-limit authentication endpoints, validate and sanitize all input, use parameterized queries (never concatenate SQL), hash passwords with bcrypt (never store plain text), use short-lived JWTs with refresh tokens, set httpOnly and secure flags on cookies, keep dependencies updated (npm audit), and never expose stack traces or internal errors to clients.
Performance optimization starts with understanding that Node.js is best for I/O-bound workloads. Key tips include: use connection pooling for databases, implement caching with Redis for frequently accessed data, use streams for large data processing (file uploads, CSV exports), enable gzip compression, avoid synchronous operations in request handlers, use pagination for large datasets, and offload CPU-intensive work to worker threads or background jobs. Monitoring tools like PM2, New Relic, or Prometheus help identify bottlenecks before they become outages.
Reliable error handling separates amateur code from production code. Every async operation should have error handling. Unhandled promise rejections and uncaught exceptions should be caught at the process level. Use a centralized error handler in Express. Log errors with structured metadata (request ID, user ID, stack trace). Monitor error rates and set up alerts for spikes. Implement health check endpoints that verify database connectivity, cache availability, and external service health.
// Feature-based (modular) structure
const projectStructure = `
server/
src/
index.ts # App entry, middleware setup
middleware/
auth.middleware.ts # JWT verification
error.middleware.ts # Centralized error handler
upload.middleware.ts # File upload handling
module/
auth/
auth.routes.ts # Route definitions
auth.controller.ts# Request/response handling
auth.service.ts # Business logic
auth.validation.ts# Input validation
job/
job.routes.ts
job.controller.ts
job.service.ts
student/
student.routes.ts
student.controller.ts
student.service.ts
database/
db.ts # Prisma client singleton
prisma/
schema.prisma # Database schema
migrations/ # Migration files
utils/
jwt.utils.ts # Token helpers
password.utils.ts # Bcrypt helpers
s3.utils.ts # File upload helpers
types/
express.d.ts # Type extensions
`;
console.log('Structure benefits:');
console.log('1. Feature code co-located (routes + controller + service)');
console.log('2. Easy to add new features (copy module template)');
console.log('3. Clear dependency direction (routes -> controller -> service -> db)');
console.log('4. Shared utilities in /utils, shared middleware in /middleware');
Each module contains all related code: routes define endpoints, controllers handle HTTP concerns, services contain business logic, and validation handles input checking. This separation makes code testable (services can be tested without HTTP) and maintainable (changes to one feature do not touch others).
const express = require('express');
const app = express();
// Health check endpoint
app.get('/health', async (req, res) => {
const checks = {
uptime: process.uptime(),
memory: process.memoryUsage(),
timestamp: Date.now()
};
try {
// Check database
// await prisma.$queryRaw`SELECT 1`;
checks.database = 'connected';
} catch (e) {
checks.database = 'disconnected';
}
try {
// Check Redis
// await redis.ping();
checks.cache = 'connected';
} catch (e) {
checks.cache = 'disconnected';
}
const isHealthy = checks.database === 'connected';
res.status(isHealthy ? 200 : 503).json({
status: isHealthy ? 'healthy' : 'degraded',
checks
});
});
// Metrics endpoint
app.get('/metrics', (req, res) => {
const mem = process.memoryUsage();
const metrics = {
uptime_seconds: Math.floor(process.uptime()),
memory_heap_used_mb: Math.round(mem.heapUsed / 1024 / 1024),
memory_heap_total_mb: Math.round(mem.heapTotal / 1024 / 1024),
memory_rss_mb: Math.round(mem.rss / 1024 / 1024),
event_loop_lag_ms: 0, // Measure with perf_hooks in production
pid: process.pid,
node_version: process.version
};
res.json(metrics);
});
console.log('Health check: /health');
console.log('Metrics: /metrics');
console.log('Current memory:', Math.round(process.memoryUsage().heapUsed / 1024 / 1024), 'MB');
The /health endpoint verifies database and cache connectivity, returning 200 when healthy or 503 when degraded. Load balancers use this to route traffic only to healthy instances. The /metrics endpoint exposes runtime statistics for monitoring dashboards.
const express = require('express');
function createProductionApp() {
const app = express();
// ---- SECURITY ----
// 1. Security headers
// app.use(helmet());
// 2. CORS (strict in production)
// app.use(cors({ origin: config.cors.origin, credentials: true }));
// 3. Rate limiting
// app.use('/api/auth', authRateLimiter);
// app.use('/api', apiRateLimiter);
// 4. Body size limits
app.use(express.json({ limit: '10kb' }));
app.use(express.urlencoded({ extended: true, limit: '10kb' }));
// 5. Trust proxy (behind nginx/load balancer)
app.set('trust proxy', 1);
// ---- PERFORMANCE ----
// 6. Compression
// app.use(compression());
// 7. Request logging
// app.use(morgan('combined', { stream: winstonStream }));
// ---- RELIABILITY ----
// 8. Request timeout
app.use((req, res, next) => {
res.setTimeout(30000, () => {
res.status(408).json({ error: 'Request timeout' });
});
next();
});
// 9. 404 handler
app.use((req, res) => {
res.status(404).json({ error: 'Route not found' });
});
// 10. Centralized error handler
app.use((err, req, res, next) => {
const status = err.statusCode || 500;
res.status(status).json({
error: status < 500 ? err.message : 'Internal server error'
});
});
return app;
}
console.log('Production app checklist:');
console.log('Security: helmet, cors, rate-limit, body-limit, trust-proxy');
console.log('Performance: compression, logging');
console.log('Reliability: timeout, 404, error-handler');
This shows a production Express app setup with security, performance, and reliability middleware applied in the correct order. Security middleware runs first (before routes), performance middleware is global, and error handling comes last. This template is a solid starting point for any production Express API.
project structure, security checklist, performance tips, monitoring, error handling patterns, code organization