Node.js Best Practices
Comprehensive Node.js development guidelines
Based on goldbergyoni/nodebestpractices๐ฏ 102 Best Practices | ๐ฆ Node.js 22+ |
Table of Contents
- Project Structure
- Framework Selection
- TypeScript Usage
- Error Handling
- Code Style and Patterns
- Testing and Quality
- Production Best Practices
- Security Best Practices
- Docker Best Practices
- Code Examples
- Summary Checklist
Project Structure
๐ Component-Based Architecture
Structure your application by business components (bounded contexts) for better modularity and maintainability.
my-system/
โโโ apps/ # Business components
โ โโโ orders/
โ โโโ users/
โ โโโ payments/
โโโ libraries/ # Shared utilities
โโโ logger/
โโโ authenticator/
Benefits:
- โ Reduced mental overload
- โ Smaller, granular changes
- โ Faster development cycles
- โ Clear modularity borders
Anti-pattern:
- โ Mixing artifacts from different modules
- โ Tightly-coupled 'spaghetti' architecture
- โ Global dependencies everywhere
Best Practices:
- โ
Each module has its own
package.json - โ
Use
package.json.mainorpackage.json.exportsfor public API - โ Hide internal implementation details
- โ Support Monorepo or multi-repo approaches
โ๏ธ Configuration Management
Implement secure, flexible configuration from day one.
Requirements:
- โ Read from files AND environment variables
- โ Keep secrets outside committed code
- โ Hierarchical structure for easy navigation
- โ Type support and validation
- โ Default values for all keys
Recommended Libraries:
// Example with convict
const convict = require('convict');
const config = convict({
env: {
format: ['production', 'development', 'test'],
default: 'development',
env: 'NODE_ENV'
},
port: {
format: 'port',
default: 3000,
env: 'PORT'
}
});
config.validate({ allowed: 'strict' });
Framework Selection
Recommended Frameworks (2024)
- Nest.js - For OOP teams and large-scale apps
- Fastify - For microservices with simple Node.js mechanics
- Express - For simplicity and wide ecosystem
- Koa - For minimalist, modern middleware approach
Framework Considerations
- Evaluate based on team size, project scale, and architecture
- Consider TypeScript support and plugin ecosystem
- Assess performance requirements vs development speed
TypeScript Usage
Use TypeScript Sparingly and Thoughtfully
- Define variables and function return types
- Avoid over-engineering with advanced features
- Use ~50 keywords thoughtfully, not excessively
- Utilize advanced features only when truly needed
- Remember: Types catch ~20% of bugs; tests catch the rest
- Balance type safety with code simplicity
Error Handling
Async Error Handling
- Use async-await or promises exclusively
- Avoid callback-style error handling (callback hell)
- Enable better error handling with try-catch blocks
Error Object Standards
- Always extend the built-in Error object
- Add useful properties: name, code, isCatastrophic
- Use ESLint rule:
no-throw-literal - For TypeScript:
@typescript-eslint/no-throw-literal
Error Classification
-
Operational Errors: Known, expected errors (invalid input)
- Handle gracefully, log, and continue
-
Catastrophic Errors: Unknown programmer errors
- Log, alert, and restart process gracefully
Centralized Error Handling
- Create dedicated error handler object
- Don't handle errors within middleware
- Encapsulate: logging, crash decisions, monitoring
- All entry points should use the same handler
Error Documentation
- Document API errors using OpenAPI or GraphQL
- Help API callers handle errors properly
- Prevent unexpected crashes in client applications
Process Exit Strategy
- Exit gracefully on unknown errors
- Let runtime (Docker, Kubernetes) restart the process
- Log observable errors before shutdown
- Close connections properly
Logging Best Practices
- Use mature logger: Pino or Winston (avoid console.log)
- Support log levels, pretty printing, and coloring
- Write logs to stdout (let infrastructure handle routing)
- Attach custom properties with minimal serialization penalty
Testing Error Flows
- Test both positive and error scenarios
- Simulate uncaught exceptions
- Verify error handler behavior
- Test error recovery mechanisms
Monitoring
- Use APM products to discover errors automatically
- Monitor performance and downtime proactively
- Gauge application health in real-time
Promise Management
- Catch unhandled promise rejections
- Register to
process.unhandledRejectionevent - Always await promises before returning for full stacktrace
- Use
return awaitpattern in async functions
Input Validation
- Fail fast: validate arguments immediately
- Use validation libraries: ajv, zod, typebox
- Validate at API boundaries
- Assert API input to prevent nasty bugs
Event Emitter Error Handling
- Subscribe to 'error' events on event emitters
- Handle stream errors explicitly
- For EventTargets, use global process error handling
- Initialize emitters with
{captureRejections: true}for async handlers
Code Style and Patterns
ESLint Configuration
- Use ESLint as the standard linter
- Combine with Prettier for code formatting
- Catch errors and anti-patterns early
Node.js-Specific Linting
- Use plugins:
- eslint-plugin-node
- eslint-plugin-security
- eslint-plugin-mocha / eslint-plugin-jest
- eslint-plugin-require
- Detect Node.js-specific anti-patterns
Code Formatting Rules
- Opening braces on same line as statement
- Proper statement separation (use semicolons or understand ASI)
- Use ESLint semi rule
- Use no-unexpected-multiline rule
Naming Conventions
- lowerCamelCase: variables, constants, functions
- UpperCamelCase: classes
- UPPER_SNAKE_CASE: global/static variables
- Name all functions (avoid anonymous functions)
- Use descriptive but concise names
Variable Declaration
- Prefer
constoverlet - Avoid
varcompletely (ES6+) -
constprevents accidental reassignment -
letfor block-scoped variables that need reassignment
Module Management
- Require modules at file beginning
- Import before any function definitions
- Avoid requires inside functions (except lazy loading)
Module Entry Points
- Set explicit entry point (index.js or package.json.main)
- Use package.json.exports for ESM
- Hide internal structure from clients
Comparison Operators
- Use strict equality
===over== - Avoid type coercion surprises
Async Patterns
- Use async-await as primary pattern
- Avoid callbacks (callback hell)
- Promises for library compatibility
- Arrow functions for compact code
Side Effects
- Avoid effects outside of functions
- No DB/network calls at module load time
- Use factory or revealing module patterns for initialization
- Keep code testable and mockable
Testing and Quality
Test Coverage Requirements
- Write API (component) tests at minimum
- Component tests provide more coverage than unit tests
- Use tools like Postman for API testing
Test Naming Convention (3 Parts)
- What is being tested (unit under test)
- Under what circumstances
- What is the expected result
- Example: "ProductService - when no price provided - should return validation error"
Test Structure (AAA Pattern)
- Arrange: Setup test data and conditions
- Act: Execute the unit under test
- Assert: Verify expected outcome
Environment Consistency
- Use Node version management: nvm or Volta
- Specify Node version in project files
- Replicate to CI and production runtime
- Prevent version drift across environments
Test Data Management
- Avoid global test fixtures and seeds
- Add data per-test (test isolation)
- Each test manages its own database state
- Prevent test coupling and interference
Test Organization
- Tag tests by category: #cold, #api, #sanity, #integration
- Run subsets based on context (commit, PR, deploy)
- Example:
mocha --grep 'sanity'
Code Coverage
- Use Istanbul/NYC for coverage reports
- Identify untested code paths
- Set coverage thresholds in CI
- Fail builds on coverage decrease
E2E Testing
- Use production-like environment
- Leverage docker-compose for services
- Keep DBs synchronized across environments
Static Analysis
- Use SonarQube or Code Climate
- Detect code smells and complexity
- Track code quality over time
- Integrate into CI pipeline
External Service Mocking
- Mock external HTTP services: nock or Mock-Server
- Simulate errors, delays, timeouts
- Test non-happy path flows
- Avoid hitting real external services in tests
Middleware Testing
- Test middlewares in isolation
- Stub {req, res, next} objects
- Verify middleware behavior independently
Port Management
- Specify port in production
- Randomize port in testing (port 0)
- Prevent port collisions in parallel tests
Test Five Outcomes
- Response validation
- State changes (DB, memory)
- Outgoing API calls
- Message queue events
- Observability (logs, metrics)
Production Best Practices
Monitoring Strategy
- Monitor uptime, metrics, and user-facing symptoms
- Track Node.js-specific metrics (event loop lag)
- Use distributed tracing with Open Telemetry
- Implement comprehensive logging
- 4 layers: uptime, metrics, distributed flows, logs
Smart Logging
- Plan logging platform from day 1
- Log to stdout, let environment handle routing
- Include transaction IDs (correlation IDs)
- Use AsyncLocalStorage for context propagation
- Enable log aggregation and analysis
Reverse Proxy Delegation
- Delegate CPU-intensive tasks (gzip, SSL)
- Use nginx, HAproxy, or cloud services
- Keep Node thread focused on application logic
Dependency Management
- Lock dependencies with package-lock.json
- Commit lockfiles to version control
- Use
npm cifor consistent installs - Ensure identical code across environments
Process Management
- Use platform-native solutions (Kubernetes, systemd)
- Avoid custom process managers in modern platforms
- Let infrastructure handle restarts and health checks
- Ensure process uptime with proper tools
CPU Utilization
- Replicate Node process for all CPU cores
- Use container orchestration for scaling
- Don't rely on Node cluster module alone
- Verify all cores are utilized
Maintenance Endpoints
- Expose system information securely
- Provide memory usage, health checks
- Enable diagnostic operations
- Keep separate from public API
APM Integration
- Add Application Performance Monitoring
- Auto-discover slow transactions
- Identify performance bottlenecks
- Provide developer context for errors
Production-Ready Code
- Plan for production from day 1
- Consider maintainability early
- Design for operations and debugging
- Build with monitoring in mind
Memory Management
- Watch Node process memory usage
- V8 has soft limits (1.4GB default)
- Monitor for memory leaks
- Alert on unusual memory growth
Frontend Asset Management
- Serve frontend via nginx, S3, or CDN
- Don't serve static files through Node
- Exception: server-side rendering
- Keep Node focused on dynamic content
Stateless Architecture
- Store data in external data stores
- Avoid in-process state (sessions, cache, files)
- Enable easy horizontal scaling
- Support instance reapp-ing
Vulnerability Detection
- Use automated security scanning tools
- Monitor dependencies continuously
- Patch vulnerabilities immediately
- Scan locally and in CI/CD
Transaction ID Logging
- Assign UUID to each request
- Use AsyncLocalStorage for context
- Include in all log statements
- Enable request flow tracing
Environment Configuration
- Set NODE_ENV=production
- Enable production optimizations
- Libraries use environment for behavior
- Affects performance significantly
Deployment Strategy
- Automate deployments completely
- Use Docker with CI/CD tools
- Implement zero-downtime deployments
- Make deployments atomic and reversible
Node.js Version
- Use LTS (Long Term Support) releases
- Receive critical bug fixes and security updates
- Ensure module compatibility
- Plan upgrade paths
Log Routing
- Log to stdout exclusively
- Let execution environment handle destinations
- Don't hard-code log destinations
- Support flexible log aggregation
Security Best Practices
Linter Security Rules
- Use eslint-plugin-security
- Catch vulnerabilities during development
- Prevent eval usage and code injection
- Detect insecure patterns early
Rate Limiting
- Implement request limiting
- Prevent DOS attacks
- Use cloud load balancers, nginx, or rate-limiter-flexible
- Protect against abuse
Secret Management
- Never store plain-text secrets in code
- Use Vault, Kubernetes Secrets, or environment variables
- Encrypt secrets in source control
- Implement pre-commit hooks
- Rotate keys regularly
Query Injection Prevention
- Use ORM/ODM libraries (Sequelize, Knex, Mongoose)
- Leverage parameterized queries
- Validate user input for expected types
- Never concatenate user input into queries
Security Checklist
- Enable HTTPS everywhere
- Use helmet.js for HTTP headers
- Implement CSRF protection
- Sanitize user input
- Use secure session cookies
- Implement proper authentication
- Apply principle of least privilege
Dependency Security
- Scan for vulnerable dependencies
- Use npm audit or Snyk
- Update dependencies regularly
- Monitor security advisories
Input Validation
- Validate all user input
- Use validation libraries (joi, yup, zod)
- Implement allow-lists over deny-lists
- Validate data types and formats
Authentication Best Practices
- Use bcrypt or scrypt for passwords
- Implement 2FA where possible
- Use secure JWT practices
- Rotate secrets regularly
Data Protection
- Escape HTML, JS, and CSS output
- Validate JSON schemas
- Implement proper access controls
- Encrypt sensitive data at rest
Secure Headers
- Use helmet.js middleware
- Set Content-Security-Policy
- Enable HSTS
- Disable X-Powered-By
Error Information Leakage
- Hide error details from clients
- Log detailed errors server-side
- Return generic error messages
- Don't expose stack traces in production
Child Process Safety
- Avoid child_exec with user input
- Sanitize all arguments
- Use execFile over exec
- Implement timeouts
RegEx DOS Prevention
- Validate regex patterns
- Set timeouts for regex operations
- Test regex with long inputs
- Use safe-regex library
Session Security
- Use secure session configuration
- Set httpOnly and secure flags
- Implement session timeouts
- Rotate session IDs
NPM Security
- Enable 2FA for npm account
- Audit packages before publishing
- Use .npmignore to prevent secret leaks
- Review dependencies regularly
Module Loading Security
- Avoid dynamic requires with user input
- Use static imports when possible
- Validate module paths
- Implement sandbox for untrusted code
Redirect Safety
- Validate redirect URLs
- Use allow-list for redirects
- Prevent open redirect vulnerabilities
Built-in Module Usage
- Import built-in modules with 'node:' protocol
- Example:
import fs from 'node:fs' - Makes built-in modules explicit
- Prevents shadowing attacks
Docker Best Practices
Multi-Stage Builds
- Use multi-stage builds for smaller images
- Separate build and runtime dependencies
- Reduce attack surface
- Improve security and performance
Process Bootstrap
- Use
nodecommand directly, avoidnpm start - Faster startup, proper signal handling
- No unnecessary npm process overhead
Container Runtime
- Let orchestrator handle replication
- Use Kubernetes/Docker Swarm for uptime
- Don't run process manager inside container
Secret Protection
- Use .dockerignore effectively
- Never commit secrets to images
- Use Docker secrets or environment variables
- Clean secrets after build
Dependency Cleanup
- Run
npm ci --productionin final stage - Remove devDependencies
- Clean npm cache
- Minimize image size
Graceful Shutdown
- Handle SIGTERM properly
- Implement health checks
- Clean up connections on shutdown
- Drain requests before exit
Memory Limits
- Set Docker memory limits
- Configure V8 max-old-space-size
- Prevent OOM kills
- Match container and V8 limits
Image Caching
- Order Dockerfile for optimal caching
- Copy package.json before source code
- Leverage layer caching
- Minimize cache invalidation
Image Tagging
- Use explicit image references
- Avoid 'latest' tag in production
- Pin dependencies to versions
- Ensure reproducible builds
Base Image Selection
- Prefer Alpine or slim variants
- Minimize base image size
- Reduce vulnerabilities
- Use official Node.js images
Build Secrets
- Use BuildKit secret mounts
- Never use ARG for secrets
- Secrets don't persist in layers
- Clean build-time secrets
Image Scanning
- Scan for vulnerabilities (Snyk, Trivy)
- Check multiple layers
- Integrate into CI/CD
- Fix critical vulnerabilities
Cache Management
- Clean node_modules cache
- Remove unnecessary files
- Use .dockerignore
- Minimize layer size
Dockerfile Linting
- Use hadolint for linting
- Follow best practices
- Catch common mistakes
- Integrate into CI
Code Examples
Proper Error Handling
// Do: Use AppError extending Error
class AppError extends Error {
constructor(name, message, httpCode, isOperational = true) {
super(message);
this.name = name;
this.httpCode = httpCode;
this.isOperational = isOperational;
Error.captureStackTrace(this);
}
}
// Usage
throw new AppError('ValidationError', 'Invalid email', 400);
Async-Await Pattern
// Do: Use async-await
async function getUser(id) {
try {
const user = await userRepository.findById(id);
return user;
} catch (error) {
logger.error('Failed to get user', { id, error });
throw new AppError('UserNotFound', 'User not found', 404);
}
}
// Avoid: Callbacks
function getUser(id, callback) {
userRepository.findById(id, (err, user) => {
if (err) return callback(err);
callback(null, user);
});
}
Configuration Management
// Do: Use convict or similar
const convict = require('convict');
const config = convict({
env: {
format: ['production', 'development', 'test'],
default: 'development',
env: 'NODE_ENV'
},
port: {
format: 'port',
default: 3000,
env: 'PORT'
},
db: {
host: {
format: String,
default: 'localhost',
env: 'DB_HOST'
}
}
});
config.validate({ allowed: 'strict' });
module.exports = config.get();
Logging with Context
// Do: Use AsyncLocalStorage for transaction ID
const { AsyncLocalStorage } = require('async_hooks');
const asyncLocalStorage = new AsyncLocalStorage();
function logWithContext(message, data) {
const context = asyncLocalStorage.getStore();
logger.info(message, {
...data,
transactionId: context?.transactionId
});
}
// Middleware to set context
app.use((req, res, next) => {
const transactionId = uuid.v4();
asyncLocalStorage.run({ transactionId }, () => {
next();
});
});
Input Validation
// Do: Use validation library
const Joi = require('joi');
const userSchema = Joi.object({
email: Joi.string().email().required(),
password: Joi.string().min(8).required(),
age: Joi.number().integer().min(18).max(120)
});
function validateUser(userData) {
const { error, value } = userSchema.validate(userData);
if (error) {
throw new AppError('ValidationError', error.message, 400);
}
return value;
}
Security Headers
// Do: Use helmet
const helmet = require('helmet');
app.use(helmet());
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", 'data:', 'https:']
}
}));
Summary Checklist
- [ ] Structure by business components
- [ ] Use 3-tier layering (entry-points, domain, data-access)
- [ ] Implement centralized error handling
- [ ] Use async-await exclusively
- [ ] Configure ESLint with security plugins
- [ ] Write API tests minimum
- [ ] Use mature logger (Pino/Winston)
- [ ] Implement rate limiting
- [ ] Validate all user input
- [ ] Lock dependencies
- [ ] Use environment variables for config
- [ ] Monitor with APM tools
- [ ] Set NODE_ENV=production
- [ ] Log to stdout only
- [ ] Use Docker multi-stage builds
- [ ] Implement graceful shutdown
- [ ] Scan for vulnerabilities
- [ ] Use LTS Node.js version
- [ ] Enable TypeScript thoughtfully
- [ ] Test error flows
References
- Main Repository: https://github.com/goldbergyoni/nodebestpractices
- JavaScript Testing: https://github.com/goldbergyoni/javascript-testing-best-practices
- Node.js Integration Tests: https://github.com/testjavascript/nodejs-integration-tests-best-practices
Top comments (0)