AgentSkillsCN

express-best-practices

Express.js与MongoDB/Mongoose的最佳实践与反模式目录。在编写、审查或重构Express控制器、中间件、路由、Mongoose模型,或API端点时使用此功能。涵盖MVC模式、错误处理、安全防护、验证机制,以及代码组织。

SKILL.md
--- frontmatter
name: express-best-practices
description: "Express.js and MongoDB/Mongoose best practices and anti-pattern catalog. Use when writing, reviewing, or refactoring Express controllers, middleware, routes, Mongoose models, or API endpoints. Covers MVC patterns, error handling, security, validation, and code organization."

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 asyncHandler wrapper 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 errorHandler middleware
  • 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.log for error logging — use the project loggers

Mongoose Models (HIGH)

Schema Design

  • Define explicit types, required fields, and defaults for every field
  • Use timestamps: true for automatic createdAt/updatedAt
  • Add indexes for fields used in queries (especially in find filters and sorts)
  • Use enum for fields with fixed value sets
  • Set trim: true on 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 findOneAndUpdate without { 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 req or 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.user for 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.body is undefined (not {}) when no body is sent — always use optional chaining: req.body?.title
  • Async route handlers auto-propagate errors to next(err)asyncHandler still recommended for consistency
  • Path syntax changed: /foo*/foo(.*), /:file.:ext?/:file{.:ext}

Mongoose 8 Specifics (HIGH)

  • rawResult is removed — use includeResultMetadata() for findOneAndUpdate metadata
  • doc.deleteOne() returns a Query, not a Promise — call .exec() or await it explicitly
  • findOneAndRemove() is removed — use findOneAndDelete() only
  • count() is removed — use countDocuments() or estimatedDocumentCount()

NoSQL Injection Prevention (CRITICAL)

  • NEVER pass raw req.body or req.query values as Mongoose query operators
  • A client can send { "password": { "$ne": "" } } to bypass auth — always validate input types
  • Avoid $where (executes arbitrary JS), $regex with untrusted input, and $expr with 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, and prototype keys from user input
  • Prefer explicit field extraction: const { title, content } = req.body

Graceful Shutdown (HIGH)

  • Handle SIGTERM and SIGINT signals
  • 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 } covers status-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: error for failures, warn for recoverable issues, info for 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 compression middleware for API responses (or handle at reverse proxy level)
  • Set appropriate Cache-Control headers for static assets and GET endpoints
  • Use ETag headers to enable 304 Not Modified responses
  • 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 of console.log
  • Max ~200 lines per file — extract helpers if larger
  • ES Modules only — no require()