Best practices, coding conventions, and patterns for Node.js backend projects. Use this skill when writing code, creating tests, or implementing new features to ensure consistency across all backend projects.
Install
npx skillscat add marco-meini/cursor/backend-nodejs-best-practices Install via the SkillsCat registry.
Node.js Backend - Best Practices & Skills
This skill provides comprehensive guidance on technical skills, patterns, and best practices required to work effectively on Node.js backend projects across the organization.
When to Use
- Use this skill when writing new controllers, models, or utilities
- Use this skill when creating or updating tests
- Use this skill when implementing new features to ensure consistency
- Use this skill when reviewing code to verify adherence to project standards
- Use this skill when refactoring existing code
Core Technologies
Runtime & Language
- Node.js: ES modules (
.mjsextension), native ESM syntax - JavaScript: Modern ES6+ features, async/await patterns
- No CommonJS: Do not use
requireormodule.exports
Testing Framework
- Mocha: Test runner for unit and integration tests
- Chai: Assertion library (
expect,assert) - Sinon: Stubs, spies, and mocks for test isolation
- Test Structure: Top-level
describeper class, nesteddescribeper method,itblocks for test cases
Web Framework
- Express.js: HTTP server and routing
- Middleware Pattern: Authentication, validation, error handling
Architecture Patterns
Project Structure
Typical structure for Node.js backend projects:
app/
controllers/ # HTTP layer (routing + minimal orchestration)
lib/ # Pure/domain utilities (stateless where possible)
model/ # Data access (Postgres / Mongo wrappers)
cronie/ # Scheduled / batch jobs (if applicable)
config/ # Environment-specific configuration
_db/ # SQL alignment & migration scripts (if applicable)
docs/ # OpenAPI fragments + bundling assets (if applicable)
test/ # Test files mirroring source structureNote: Some directories may not be present in all projects; adapt structure to project needs.
Controller Pattern
- Use base controller class pattern (e.g.,
Abstract_Controller) when available - Register routes in constructor
- Bind handlers:
this.__method.bind(this) - Private methods prefixed with
__(double underscore) - Input validation → domain/model calls → response shaping
- Always wrap async logic in try/catch, delegate errors to
next(error)
Data Layer
- PostgreSQL: Access via connection manager (e.g.,
env.pgConnection) and model layer (e.g.,env.pgModels) - MongoDB: Access via client manager (e.g.,
env.mongoClient) and model layer (e.g.,env.mongoModels) - Redis: Session management and caching (when applicable)
- Parameterized Queries: Always use parameterized queries, never string concatenation
- SQL Migrations: Store in
_db/<YYYYMM>_<FeatureName>/folders (when applicable)
Code Style & Conventions
Formatting
- Indentation: 2 spaces, no tabs
- Strings: Single quotes unless template literals required
- Imports: Absolute package names first, then relative paths
- Variables:
constfor immutable bindings,letfor reassignable, nevervar
Naming Conventions
- Files: kebab-case for controllers (
auth.controller.mjs) - Classes: PascalCase (
AuthController,DateUtils) - Private Methods: Double underscore prefix (
__login) - Constants: UPPER_SNAKE_CASE (
DEFAULT_LIMIT) - Config Keys: lowerCamelCase
Code Organization
- Early returns for validation & error conditions
- Keep functions cohesive and small
- Extract reusable helpers to
app/lib/ - Avoid deep nesting
Error Handling
Error Patterns
- Use
HttpResponseStatusconstants, never hardcode numeric codes - Create structured errors with
.statusand optional.errorsarray - Validation failures:
error.status = HttpResponseStatus.MISSING_PARAMS - Never expose stack traces or raw database errors in responses
- Always propagate errors via
next(error), never swallow them
Error Object Shape
const error = new Error('Validation failed');
error.status = HttpResponseStatus.MISSING_PARAMS;
error.errors = [
{ message: 'username is required' },
{ message: 'password must contain at least 8 chars' }
];
throw error;HTTP Responses
- Use
HttpResponseStatusconstants - Empty 200:
response.send() - No content:
response.sendStatus(HttpResponseStatus.NO_CONTENT) - JSON responses:
response.json(data) - Prefer deterministic ordering in list queries
Validation
- Reuse validators from
app/lib/utils.mjs - Validate early at controller entry
- Respond with
MISSING_PARAMSif required fields absent/malformed - Add new validators to
app/lib/utils.mjsif generic
Async & Promises
- Use
async/awaitsyntax - Avoid mixing with raw
.then()chains - Wrap awaited blocks in try/catch at controller boundaries
- Use
Promise.allfor parallel operations when appropriate
Logging
- Use
this.env.loggerorenv.logger - Levels:
error(failures),warn(unexpected but tolerated),info(notable events),debug(verbose) - Never log sensitive information (passwords, tokens, private keys)
- Pattern:
logger.error(context, error.stack || error.message)
Security Practices
- Never log raw JWTs (log presence of token or user id)
- Hash & compare passwords only in model/service layer
- Validate and sanitize all user-provided inputs
- Use parameterized queries exclusively
- Never hardcode secrets in source code
- Centralize secrets in environment variables or secret manager
Database Patterns
PostgreSQL
- All calls through
env.pgModels.<model>methods - Method naming: verbs (
getUserByUsername,checkPassword) - For UPDATE operations: Always use
updateByKeyinstead of raw SQL UPDATE queries- Pattern:
await env.pgConnection.updateByKey(object, fieldsToUpdate, keyFields, tableName, transaction?) - Example:
const recordToUpdate = { id_vd: room.id_vd, name_vd: data.name, data_vd: JSON.stringify(data.authorization) }; await env.pgConnection.updateByKey(recordToUpdate, ['name_vd', 'data_vd'], ['id_vd'], 'videocalls_vd', transaction);
- Pattern:
- Read-only queries (SELECT, COUNT): Use
isSlave: trueto route queries to read replicas- Reduces load on master database
- Always use for COUNT queries and read-only SELECT operations
- Pattern:
await env.pgConnection.queryReturnFirst({ sql, replacements, isSlave: true }) - Example:
const countResult = await this.env.pgConnection.queryReturnFirst({ sql: countSql, replacements: filter.replacements, isSlave: true }); - Never use
isSlave: truefor: INSERT, UPDATE, DELETE, or any write operations
- Avoid duplicate queries: If the same SQL query (or very similar) appears in multiple places, extract it to a model method
- Check for duplicate queries before writing new ones
- Create reusable methods in the appropriate model class (extending
Abstract_PgModel) - Register the model in
app/model/postgres/pg-models.mjsif it doesn't exist - Example: If querying for customer users appears multiple times, create
CustomersModel.getActiveUserIds(customerId) - Benefits: DRY principle, easier maintenance, better testability
- SQL changes in
_db/<YYYYMM>_<FeatureName>/folders - Use
prerelease.sqlfor schema modifications - Use
alignment.sqlfor data corrections - Provide idempotent scripts (guard with
IF NOT EXISTS)
Data Sanitization
- Avoid leaking raw model objects with internal fields
- Sanitize in controller before responding
- Remove sensitive fields like
password_us, internal flags
External Services
Integration Patterns
- Wrap each external integration in dedicated environment service
- Do not inline credentials or client creation in controllers
- On failures: log and proceed gracefully if not critical
- Document any degraded capability
External Services Integration
Common services that may be integrated (project-specific):
- Redis: Session management and caching
- Cloud Storage: File storage (AWS S3, Google Cloud Storage, etc.)
- Email Services: Email delivery (SparkPost, SendGrid, SES, etc.)
- Message Queues: Async processing (RabbitMQ, Redis Pub/Sub, etc.)
- Third-party APIs: Chat, video conferencing, payment gateways, etc.
- Always wrap external service calls in try/catch with proper error handling
Testing Skills
Test Structure
describe('ClassName', () => {
describe('methodName', () => {
describe('Success', () => {
it('should <expected behavior>', async () => {
// Arrange
// Act
// Assert
});
});
describe('Error', () => {
it('should propagate database errors', async () => {
// Error test
});
});
});
});Testing Patterns
- Use
sinonfor stubs/spies/mocks - Prefer
sinon.assert.calledWithover manual argument inspection - No
chai-as-promised; use try/catch for async error assertions - Stub prototype methods when code instantiates classes internally
- Never import production secrets or mutate real environment configs
- Do not test raw SQL strings; assert query method calls with expected shape/replacements
- Test all branches: happy path, conditional logic, error propagation
Test Coverage Requirements
- Happy path
- Each branch of conditional logic
- Error propagation (ensuring
nextcalled with error) - Edge cases: empty, null, invalid format, boundary values
- Side effects: status codes, response method calls, service calls, argument ordering
Configuration Management
- Do not read
process.envdirectly in controllers - Centralize in Environment/config layer
- Update all environment files when adding config entries
- Document defaults in
config/config.mjs
Documentation
Code Comments
- Use JSDoc for public class methods and complex private handlers
- Keep comments in English
- State the WHY for non-trivial algorithms, not just the WHAT
API Documentation
- Document all API endpoints (OpenAPI/Swagger, YAML fragments, or project-specific format)
- Ensure response codes and required parameters match controller behavior
- Keep documentation in sync with implementation
Batch Jobs & Cron
- Organize batch logic in dedicated directories (e.g.,
app/cronie/batch/,app/jobs/, etc.) - Use clear file naming conventions
- Orchestrate entry points appropriately (e.g.,
app/cronie/main-cronie.mjs, cron configuration, etc.) - Ensure idempotency where tasks may be retried
- Log start/end + summary metrics (counts, durations) at
infolevel
Internationalization
- Centralize user-facing message templates (e.g.,
assets/messages.mjs,locales/, etc.) - Keep placeholders explicit (e.g.,
${userName}) - Document placeholders
- Use project-specific i18n library if applicable
Performance & Reliability
- Avoid unnecessary awaits; use
Promise.allfor parallel operations - Implement retries with backoff for batch/cron jobs
- Configure limits for streaming/large payload uploads via
bodyParserLimit - Prefer deterministic ordering in queries to avoid flaky tests
Git Workflow
Branch Naming
feature/<short-kebab>fix/<short-kebab>chore/<short-kebab>refactor/<short-kebab>
Commit Messages
- Imperative present tense (
Add user lock check) - Include scope if useful:
controller(auth): ...
Pull Requests
- Keep PRs small & single-topic (< ~400 lines diff preferred, excluding tests)
- Before review: builds locally, tests pass, endpoints documented, no secrets exposed
Dependency Management
- Prefer built-in or existing project utilities before adding new libraries
- Justify any new dependency in PR description (benefit, size, maintenance risk)
- Avoid transitive duplication (e.g., date libraries – already using
moment)
Adding New Controllers Checklist
- Create controller file following project conventions (e.g.,
app/controllers/<name>.controller.mjs) - Extend base controller class if available (e.g.,
Abstract_Controller) - Define route via constructor or configuration
- Register routes inside constructor
- Implement handlers with validation + model calls
- Export class & register instance in main app file with mounting path
- Add tests under
test/controllers/replicating structure - Update API documentation
- Ensure error paths use HTTP status constants (never hardcode numeric codes)
Utility Classes
- Group related concerns (string ops, dates, validation) into cohesive classes
- Avoid state; all methods static unless stateful caching required
- When adding new static method: include JSDoc, edge case notes, and at least one test verifying failure branch
Common Libraries & Tools
Core Dependencies
express: Web frameworkpg: PostgreSQL clientmongodb: MongoDB clientredis: Redis clientstream-chat: GetStream chat integrationlodash: Utility functionsmoment: Date manipulationhandlebars: Template enginemulter: File upload handlingexceljs: Excel file processingcsv-parse: CSV parsingsoap: SOAP clientnode-fetch: HTTP client
Development Tools
mocha: Test runnerchai: Assertionssinon: Test doublesnodemon: Development server with auto-reload
Key Concepts
Environment Object
- Central dependency container: logger, session, config, external services
- Do not scatter creation logic
- Access via
this.envin controllers
Session Management
- Do not directly manipulate JWT/session except through
env.session.sessionManager - Use
env.session.checkAuthentication()middleware - Use
env.session.checkPermission()for authorization
PgFilter
- Use
PgFilterfromcommon-mjsfor building dynamic SQL queries - Add conditions:
where.addEqual(),where.addCondition() - Use
where.getWhere()andwhere.replacementsin queries - CRITICAL: Always use
getParameterPlaceHolder(value)for custom SQL conditions- Never manually construct parameter placeholders like
$${paramIndex} getParameterPlaceHolder()automatically adds the value toreplacementsarray and returns the correct placeholder ($1,$2, etc.)- After calling
getParameterPlaceHolder(), always updatereplacementsarray:replacements = [...filter.replacements] - Example:
// ❌ WRONG - Manual placeholder construction sql += ` AND field = $${paramIndex}`; replacements.push(value); paramIndex++; // ✅ CORRECT - Use getParameterPlaceHolder const placeholder = filter.getParameterPlaceHolder(value); sql += ` AND field = ${placeholder}`; replacements = [...filter.replacements];
- Never manually construct parameter placeholders like
Instructions
When working on any Node.js backend project, follow these guidelines:
- Always use ES modules – no CommonJS
- Test everything – controllers, utilities, models
- Validate early – at controller entry point
- Handle errors properly – propagate via
next(error) - Use constants –
HttpResponseStatus, never hardcode - Sanitize data – remove sensitive fields before responding
- Parameterize queries – never string concatenation
- Log appropriately – never sensitive information
- Document APIs – update YAML fragments
- Keep it simple – small functions, early returns, clear naming
When creating tests:
- Mirror the source structure in
test/directory - Use the test structure pattern shown above
- Cover all branches and edge cases
- Use sinon for mocking and stubbing
- Never modify production code just for testability
When writing controllers:
- Use base controller pattern when available (e.g., extend
Abstract_Controller) - Register routes in constructor
- Use private methods with
__prefix - Validate input early
- Handle errors with try/catch and
next(error) - Use HTTP status constants (never hardcode numeric codes)
When working with databases:
- Use model layer when available (e.g.,
env.pgModels,env.mongoModels) - Always use parameterized queries (never string concatenation)
- Store SQL migrations in appropriate directories (e.g.,
_db/<YYYYMM>_<FeatureName>/) - Sanitize data before responding
- Use read replicas (
isSlave: true) for read-only queries when available