Express Best Practices & Anti-Patterns
Modern Express 5 + Mongoose 8 conventions. Covers what to do and what to avoid. For code examples, see examples.md.
Severity Levels
- •CRITICAL — Will cause bugs, security vulnerabilities, or data loss
- •HIGH — Will cause reliability issues or scaling problems
- •MEDIUM — Will hurt maintainability or developer experience
Route Design (HIGH)
- •Routes ONLY define endpoints and attach middleware — no business logic
- •Use
asyncHandlerwrapper on ALL async route handlers - •Group routes by resource:
/api/blogs,/api/comments,/api/admin - •Use proper HTTP methods: GET for reads, POST for creates, PUT/PATCH for updates, DELETE for deletes
- •Return appropriate status codes: 200 OK, 201 Created, 400 Bad Request, 401 Unauthorized, 404 Not Found, 500 Internal Server Error
Controller Design (HIGH)
- •Controllers handle request/response only — delegate business logic to models/helpers
- •Use response helpers (
sendSuccess,sendError,sendData) for ALL responses - •Never use raw
res.status().json()— go through the response helpers - •Keep controllers thin: extract complex logic to helper functions
- •One controller file per resource (blogController, commentController, adminController)
Error Handling (CRITICAL)
- •ALWAYS use
asyncHandler— never write manual try-catch in route handlers - •Let errors propagate to the centralized
errorHandlermiddleware - •Do NOT catch errors only to rethrow them — let the middleware chain handle it
- •Throw errors with meaningful messages using
constants/messages.js - •The errorHandler middleware MUST be mounted last
Error Anti-Patterns
- •NEVER swallow errors silently (empty catch blocks)
- •NEVER send raw error objects to the client (security risk)
- •NEVER use
console.logfor error logging — use the project loggers
Mongoose Models (HIGH)
Schema Design
- •Define explicit types, required fields, and defaults for every field
- •Use
timestamps: truefor automaticcreatedAt/updatedAt - •Add indexes for fields used in queries (especially in
findfilters and sorts) - •Use
enumfor fields with fixed value sets - •Set
trim: trueon string fields to avoid whitespace issues
Query Patterns
- •Use
.lean()for read-only queries (returns plain objects, faster) - •Use
.select()to return only needed fields - •Use
.populate()sparingly — prefer denormalization for frequently accessed data - •Paginate ALL list queries — never return unbounded results
- •Use
.countDocuments()for counts, not.find().length
Model Anti-Patterns
- •NEVER store derived data that can be computed from other fields
- •NEVER skip validation by using
{ validateBeforeSave: false } - •NEVER use
findOneAndUpdatewithout{ new: true }if you need the updated document
Middleware (HIGH)
Design Principles
- •Single responsibility: one concern per middleware file
- •Middleware should be pure functions that transform
reqor enforce policies - •Order matters — security before parsing before auth before routes before errors
Auth Middleware
- •Verify JWT token from
Authorization: Bearer <token>header - •Attach decoded user to
req.userfor downstream handlers - •Return 401 for missing/invalid tokens — never silently skip
- •Protect admin routes; leave public routes unprotected
Validation Middleware
- •Validate ALL user input before it reaches the controller
- •Return 400 with clear error messages for invalid input
- •Validate body, query params, and URL params separately
Security (CRITICAL)
- •Helmet: Always use for security headers
- •CORS: Restrict to known origins; never use
cors({ origin: '*' })in production - •Rate Limiting: Apply to auth endpoints and public APIs
- •Password Hashing: Always use bcryptjs; NEVER store plain-text passwords
- •JWT: Use short expiration times; store secrets in environment variables
- •Input Sanitization: Never trust user input; validate and sanitize everything
- •File Uploads: Validate file type and size; never allow arbitrary file execution
API Response Format (MEDIUM)
Maintain consistent response shapes:
code
Success: { success: true, message: "...", ...data }
Error: { success: false, message: "..." }
List: { success: true, count: N, ...data }
Express 5 Specifics (HIGH)
- •
req.bodyisundefined(not{}) when no body is sent — always use optional chaining:req.body?.title - •Async route handlers auto-propagate errors to
next(err)—asyncHandlerstill recommended for consistency - •Path syntax changed:
/foo*→/foo(.*),/:file.:ext?→/:file{.:ext}
Mongoose 8 Specifics (HIGH)
- •
rawResultis removed — useincludeResultMetadata()forfindOneAndUpdatemetadata - •
doc.deleteOne()returns a Query, not a Promise — call.exec()orawaitit explicitly - •
findOneAndRemove()is removed — usefindOneAndDelete()only - •
count()is removed — usecountDocuments()orestimatedDocumentCount()
NoSQL Injection Prevention (CRITICAL)
- •NEVER pass raw
req.bodyorreq.queryvalues as Mongoose query operators - •A client can send
{ "password": { "$ne": "" } }to bypass auth — always validate input types - •Avoid
$where(executes arbitrary JS),$regexwith untrusted input, and$exprwith user data - •Sanitize or reject any input containing
$prefixed keys
Prototype Pollution Prevention (CRITICAL)
- •NEVER use
Object.assign({}, userInput)or spread untrusted objects without validation - •Reject or strip
__proto__,constructor, andprototypekeys from user input - •Prefer explicit field extraction:
const { title, content } = req.body
Graceful Shutdown (HIGH)
- •Handle
SIGTERMandSIGINTsignals - •Stop accepting new connections with
server.close() - •Close database connection with
mongoose.connection.close() - •Set a forced shutdown timeout (e.g., 10 seconds) as a safety net
- •Finish in-flight requests before exiting
Database Connection Resilience (HIGH)
- •Use explicit connection options:
serverSelectionTimeoutMS,retryWrites,maxPoolSize - •Handle connection errors and reconnection gracefully
- •Close connections on process shutdown — don't leave orphaned connections
MongoDB Indexing Strategy (HIGH)
- •Follow the ESR rule for compound indexes: Equality → Sort → Range
- •A compound index supports prefix subsets (e.g.,
{ status: 1, createdAt: -1 }coversstatus-only queries) - •Use
.explain("executionStats")to verify index usage in development - •Avoid low-cardinality-only indexes (e.g., boolean fields alone) — combine with other fields
Structured Logging (MEDIUM)
- •Use JSON-formatted logs with context fields (userId, requestId, durationMs)
- •NEVER log passwords, tokens, API keys, or PII
- •Use log levels appropriately:
errorfor failures,warnfor recoverable issues,infofor request lifecycle - •In production, use structured loggers (Pino/Winston) — not
console.log
JWT Best Practices (HIGH)
- •Use short-lived access tokens (15-60 min) and longer-lived refresh tokens (7-14 days)
- •Rotate refresh tokens on each use (issue new, invalidate old)
- •For token revocation: maintain a blacklist of token IDs (
jti) with TTL matching token expiry - •Prefer HttpOnly cookies for token storage when the client is same-origin
File Upload Security (HIGH)
- •Validate file content by magic bytes (not just extension or MIME type — those are spoofable)
- •Treat SVGs as potential XSS vectors (can contain
<script>tags) - •Generate random filenames server-side — never trust user-provided filenames
- •Store uploads outside the webroot or use a dedicated storage service
- •Strip EXIF and metadata when possible
Performance (MEDIUM)
- •Use
compressionmiddleware for API responses (or handle at reverse proxy level) - •Set appropriate
Cache-Controlheaders for static assets and GET endpoints - •Use
ETagheaders to enable304 Not Modifiedresponses - •For heavy computation, offload to worker threads — don't block the event loop
Code Organization (MEDIUM)
- •camelCase for file names (blogController.js, asyncHandler.js)
- •PascalCase for model files (Blog.js, Comment.js, User.js)
- •Keep constants in
constants/messages.js— no hardcoded strings - •Use project loggers (
dbLogger,httpLogger) instead ofconsole.log - •Max ~200 lines per file — extract helpers if larger
- •ES Modules only — no
require()