Back to list
curtbushko

nodejs-tdd

by curtbushko

3🍴 0📅 Jan 24, 2026

SKILL.md


name: nodejs-tdd description: Comprehensive Node.js development skill with best practices for project structure, coding patterns, and Test-Driven Development. Use this skill when building Node.js applications, APIs, or backend services. Covers project layout, ES modules, async patterns, error handling, security, and implements TDD with a structured workflow (Investigate → Reproduce → Test → Fix → Validate → Finalize).

Node.js TDD Development Skill

This skill provides guidance for building high-quality Node.js applications using best practices and Test-Driven Development (TDD).

TDD Workflow

IMPORTANT: Every feature implementation MUST follow this TDD workflow:

1. Investigate

  • Understand the requirement thoroughly
  • Review existing code and related modules
  • Identify dependencies and potential side effects
  • Document acceptance criteria

2. Reproduce (for bugs) / Plan (for features)

  • For bugs: Create a minimal reproduction case
  • For features: Design the interface/API before implementation
  • Identify edge cases and error scenarios
  • Plan test scenarios

3. Test

  • Write failing tests FIRST (Red phase)
  • Structure tests using AAA pattern (Arrange, Act, Assert)
  • Include positive and negative test cases
  • Test edge cases and error handling
// Test naming: describe WHAT, WHEN, and EXPECTED outcome
describe('UserService', () => {
  describe('createUser', () => {
    it('should create user when valid email provided', async () => {
      // Arrange
      const userData = { email: 'test@example.com', name: 'Test' };
      
      // Act
      const result = await userService.createUser(userData);
      
      // Assert
      expect(result.id).toBeDefined();
      expect(result.email).toBe(userData.email);
    });

    it('should throw ValidationError when email is invalid', async () => {
      // Arrange
      const userData = { email: 'invalid', name: 'Test' };
      
      // Act & Assert
      await expect(userService.createUser(userData))
        .rejects.toThrow(ValidationError);
    });
  });
});

4. Fix / Implement

  • Write minimal code to pass the test (Green phase)
  • Follow SOLID principles
  • Keep functions small and focused
  • Use async/await for asynchronous code

5. Validate

  • Run all tests to ensure no regressions
  • Check code coverage (aim for >80%)
  • Run linter (ESLint) to catch issues
  • Review error handling completeness

6. Finalize

  • Refactor for clarity and maintainability
  • Remove dead code and console.logs
  • Update documentation if needed
  • Commit with descriptive message

Project Structure

Use component-based architecture with clear separation of concerns:

project-root/
├── src/
│   ├── components/           # Business domain modules
│   │   ├── users/
│   │   │   ├── user.controller.js
│   │   │   ├── user.service.js
│   │   │   ├── user.repository.js
│   │   │   ├── user.model.js
│   │   │   ├── user.routes.js
│   │   │   ├── user.validation.js
│   │   │   └── __tests__/
│   │   │       ├── user.service.test.js
│   │   │       └── user.controller.test.js
│   │   ├── orders/
│   │   └── payments/
│   ├── middleware/           # Express/Fastify middleware
│   ├── config/               # Configuration management
│   │   └── index.js
│   ├── utils/                # Shared utilities
│   ├── errors/               # Custom error classes
│   └── app.js                # Application setup
├── tests/
│   ├── integration/          # API/Integration tests
│   ├── e2e/                  # End-to-end tests
│   └── fixtures/             # Test data factories
├── .env.example
├── .nvmrc                    # Node version specification
├── package.json
├── package-lock.json
└── jest.config.js

ES Modules

Use ES Modules (ESM) as the default module system:

// package.json
{
  "type": "module",
  "engines": {
    "node": ">=20.0.0"
  }
}
// Use import/export syntax
import { createServer } from 'node:http';
import express from 'express';
import { UserService } from './user.service.js';

export const userService = new UserService();

Import built-in modules with node: protocol:

import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import { EventEmitter } from 'node:events';

Async Patterns

Always use async/await

// ✅ Good
async function getUser(id) {
  const user = await userRepository.findById(id);
  if (!user) {
    throw new NotFoundError(`User ${id} not found`);
  }
  return user;
}

// ❌ Avoid callbacks
function getUser(id, callback) {
  userRepository.findById(id, (err, user) => {
    if (err) return callback(err);
    callback(null, user);
  });
}

Always await before returning promises

// ✅ Good - full stack trace preserved
async function processOrder(orderId) {
  return await orderService.process(orderId);
}

// ❌ Avoid - partial stack trace
async function processOrder(orderId) {
  return orderService.process(orderId);
}

Handle Promise rejections

process.on('unhandledRejection', (reason, promise) => {
  logger.error('Unhandled Rejection:', { reason, promise });
  // Graceful shutdown
  process.exit(1);
});

Error Handling

Extend built-in Error

// src/errors/app-error.js
export class AppError extends Error {
  constructor(message, statusCode = 500, isOperational = true) {
    super(message);
    this.name = this.constructor.name;
    this.statusCode = statusCode;
    this.isOperational = isOperational;
    Error.captureStackTrace(this, this.constructor);
  }
}

export class NotFoundError extends AppError {
  constructor(message = 'Resource not found') {
    super(message, 404);
  }
}

export class ValidationError extends AppError {
  constructor(message, errors = []) {
    super(message, 400);
    this.errors = errors;
  }
}

Centralized error handler

// src/middleware/error-handler.js
export function errorHandler(err, req, res, next) {
  logger.error({
    message: err.message,
    stack: err.stack,
    path: req.path,
    method: req.method,
  });

  if (err.isOperational) {
    return res.status(err.statusCode).json({
      error: err.message,
      ...(err.errors && { details: err.errors }),
    });
  }

  // Programmer error - don't leak details
  res.status(500).json({ error: 'Internal server error' });
}

Subscribe to EventEmitter errors

const emitter = new EventEmitter({ captureRejections: true });
emitter.on('error', (err) => {
  logger.error('EventEmitter error:', err);
});

Configuration

Use environment-aware, hierarchical configuration:

// src/config/index.js
import { z } from 'zod';

const configSchema = z.object({
  NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
  PORT: z.coerce.number().default(3000),
  DATABASE_URL: z.string().url(),
  JWT_SECRET: z.string().min(32),
  LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
});

const config = configSchema.parse(process.env);
export default config;

Testing Best Practices

Testing setup

// jest.config.js
export default {
  testEnvironment: 'node',
  transform: {},
  coverageThreshold: {
    global: { branches: 80, functions: 80, lines: 80, statements: 80 },
  },
  setupFilesAfterEnv: ['./tests/setup.js'],
};

Test file naming

  • Unit tests: *.test.js (co-located with source)
  • Integration tests: *.integration.test.js
  • E2E tests: *.e2e.test.js

Test data isolation

// Each test manages its own data
beforeEach(async () => {
  await db.truncate(['users', 'orders']);
});

it('should find user by email', async () => {
  // Arrange - test creates its own data
  const user = await createTestUser({ email: 'test@example.com' });
  
  // Act
  const found = await userService.findByEmail('test@example.com');
  
  // Assert
  expect(found.id).toBe(user.id);
});

Mock external services

import nock from 'nock';

beforeEach(() => {
  nock('https://api.external.com')
    .get('/users/123')
    .reply(200, { id: '123', name: 'Test User' });
});

afterEach(() => {
  nock.cleanAll();
});

Randomize test port

// Start server with random port for parallel tests
const server = app.listen(0);
const { port } = server.address();

Code Style

Use ESLint with security plugins

// .eslintrc.json
{
  "extends": [
    "eslint:recommended",
    "plugin:security/recommended",
    "plugin:node/recommended"
  ],
  "plugins": ["security"],
  "rules": {
    "no-throw-literal": "error",
    "prefer-const": "error",
    "no-var": "error"
  }
}

Naming conventions

  • lowerCamelCase for variables and functions
  • UpperCamelCase for classes
  • UPPER_SNAKE_CASE for constants
  • Name all functions (no anonymous functions)

Icons and symbols

  • Use Nerd Fonts instead of emojis for console output, CLI tools, and logging
  • Nerd Fonts provide consistent rendering across terminals and editors
  • Examples: , , , , , , ``
  • Install: https://www.nerdfonts.com/
// ✅ Good - Nerd Font icons
console.log(' Server started on port 3000');
console.log(' User created successfully');
console.log(' Connection failed');
console.log(' Running tests...');

// ❌ Avoid - Emojis (inconsistent rendering)
console.log('🚀 Server started on port 3000');
console.log('✅ User created successfully');

Function structure

// ✅ Small, focused functions with descriptive names
async function validateUserEmail(email) {
  const existing = await userRepository.findByEmail(email);
  if (existing) {
    throw new ValidationError('Email already registered');
  }
  return true;
}

// ✅ Avoid effects outside functions
// Bad: runs on import
const db = connectToDatabase();

// Good: explicit initialization
export function initializeDatabase() {
  return connectToDatabase();
}

Security

Input validation

import { z } from 'zod';

const createUserSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8).max(100),
  name: z.string().min(1).max(255),
});

export function validateCreateUser(data) {
  return createUserSchema.parse(data);
}

Rate limiting

import rateLimit from 'express-rate-limit';

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100,
  standardHeaders: true,
});

app.use('/api/', limiter);

Password hashing

import bcrypt from 'bcrypt';

const SALT_ROUNDS = 12;

export async function hashPassword(password) {
  return bcrypt.hash(password, SALT_ROUNDS);
}

export async function verifyPassword(password, hash) {
  return bcrypt.compare(password, hash);
}

Helmet for HTTP headers

import helmet from 'helmet';
app.use(helmet());

Logging

Use structured logging with Pino:

import pino from 'pino';

export const logger = pino({
  level: config.LOG_LEVEL,
  formatters: {
    level: (label) => ({ level: label }),
  },
  // Production: JSON to stdout
  // Development: pretty print
  transport: config.NODE_ENV === 'development'
    ? { target: 'pino-pretty' }
    : undefined,
});

Add transaction IDs

import { AsyncLocalStorage } from 'node:async_hooks';
import { randomUUID } from 'node:crypto';

const asyncLocalStorage = new AsyncLocalStorage();

export function requestIdMiddleware(req, res, next) {
  const requestId = req.headers['x-request-id'] || randomUUID();
  asyncLocalStorage.run({ requestId }, () => {
    req.requestId = requestId;
    res.setHeader('x-request-id', requestId);
    next();
  });
}

export function getRequestId() {
  return asyncLocalStorage.getStore()?.requestId;
}

Graceful Shutdown

async function shutdown(signal) {
  logger.info(`Received ${signal}, starting graceful shutdown`);
  
  // Stop accepting new requests
  server.close();
  
  // Wait for existing requests to complete
  await new Promise((resolve) => {
    server.on('close', resolve);
  });
  
  // Close database connections
  await db.end();
  
  logger.info('Graceful shutdown complete');
  process.exit(0);
}

process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));

Dependencies

Lock versions

Always commit package-lock.json and use npm ci in CI/CD.

Audit regularly

npm audit
npm outdated

Node version management

Create .nvmrc file:

20.11.0

Quick Reference

CategoryDoDon't
ModulesES Modules with node: prefixCommonJS require()
Asyncasync/awaitCallbacks
Variablesconst, then letvar
ErrorsCustom Error classesThrowing strings
ConfigEnvironment variables + validationHardcoded values
TestsTDD with AAA patternNo tests / tests after
SecurityValidate all inputsTrust user input

Additional Resources

  • For testing patterns: See references/testing-patterns.md
  • For API design: See references/api-design.md

Score

Total Score

50/100

Based on repository quality metrics

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

0/10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 100以上

0/15
最近の活動

3ヶ月以内に更新がある

0/10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

0/5

Reviews

💬

Reviews coming soon