Skip to content
express-production logo

Express.js - Production Web Framework

express-production

Production-ready Express.js development covering middleware architecture, error handling, security hardening, testing strategies, and deployment patterns

mattnigh/skills_collection0installs24stars

SKILL.md

Full skill instructions

Express.js - Production Web Framework

Overview

Express is a minimal and flexible Node.js web application framework providing a robust set of features for web and mobile applications. This skill covers production-ready Express development including middleware architecture, structured error handling, security hardening, comprehensive testing, and deployment strategies.

Key Features:

  • Flexible middleware architecture with composition patterns
  • Centralized error handling with async support
  • Security hardening (Helmet, CORS, rate limiting, input validation)
  • Comprehensive testing with Supertest
  • Production deployment with PM2 clustering
  • Environment-based configuration
  • Structured logging and monitoring
  • Graceful shutdown patterns
  • Zero-downtime deployments

Installation:

# Basic Express
npm install express

# Production stack
npm install express helmet cors express-rate-limit express-validator
npm install morgan winston compression
npm install dotenv

# Development tools
npm install -D nodemon supertest jest

# Optional: Database and auth
npm install mongoose jsonwebtoken bcrypt

When to Use This Skill

Use this comprehensive Express skill when:

  • Building production REST APIs
  • Creating microservices architectures
  • Implementing secure web applications
  • Need flexible middleware composition
  • Require comprehensive error handling
  • Building systems requiring extensive testing
  • Deploying high-availability services
  • Need granular control over request/​response lifecycle

Express vs Other Frameworks:

  • Express: Maximum flexibility, unopinionated, extensive ecosystem
  • Fastify: Performance-focused, schema-based validation
  • Koa: Modern async/​await, minimalist
  • NestJS: TypeScript-first, opinionated, enterprise patterns

Quick Start

Minimal Express Server

// server.js
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;

// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// Routes
app.get('/', (req, res) => {
  res.json({ message: 'Hello World' });
});

app.get('/​health', (req, res) => {
  res.json({ status: 'ok', uptime: process.uptime() });
});

// Error handler
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ error: 'Internal server error' });
});

// Start server
const server = app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

// Graceful shutdown
process.on('SIGTERM', () => {
  console.log('SIGTERM received, closing server...');
  server.close(() => {
    console.log('Server closed');
    process.exit(0);
  });
});

Run Development Server:

# Install nodemon
npm install -D nodemon

# Run with nodemon
npx nodemon server.js

# Or add to package.json
npm run dev

Production-Ready Server Structure

project/
├── src/
│   ├── app.js              # Express app factory
│   ├── server.js           # Server entry point
│   ├── config/
│   │   ├── index.js        # Configuration management
│   │   └── logger.js       # Winston logger setup
│   ├── middleware/
│   │   ├── errorHandler.js # Centralized error handling
│   │   ├── validation.js   # Input validation
│   │   ├── auth.js         # Authentication middleware
│   │   └── rateLimiter.js  # Rate limiting
│   ├── routes/
│   │   ├── index.js        # Route aggregator
│   │   ├── users.js        # User routes
│   │   └── api/            # API versioning
│   ├── controllers/
│   │   ├── userController.js
│   │   └── authController.js
│   ├── models/             # Data models
│   ├── services/           # Business logic
│   ├── utils/
│   │   ├── AppError.js     # Custom error class
│   │   └── catchAsync.js   # Async wrapper
│   └── tests/
│       ├── unit/
│       └── integration/
├── ecosystem.config.js     # PM2 configuration
├── .env.example            # Environment template
├── nodemon.json            # Nodemon config
└── package.json

Middleware Architecture

Understanding Middleware

Middleware functions are functions that have access to the request object (req), response object (res), and the next middleware function (next).

Middleware Types:

  1. Application-level: app.use() or app.METHOD()
  2. Router-level: router.use() or router.METHOD()
  3. Error-handling: Four parameters (err, req, res, next)
  4. Built-in: express.json(), express.static()
  5. Third-party: helmet, cors, morgan

Proper Middleware Order

✅ Correct Order:

const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const compression = require('compression');
const morgan = require('morgan');
const rateLimit = require('express-rate-limit');

const app = express();

// 1. Security headers (FIRST)
app.use(helmet());

// 2. CORS configuration
app.use(cors({
  origin: process.env.ALLOWED_ORIGINS?.split(',') || '*',
  credentials: true,
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
  allowedHeaders: ['Content-Type', 'Authorization']
}));

// 3. Rate limiting (before parsing)
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // limit each IP to 100 requests per windowMs
  message: 'Too many requests from this IP'
});
app.use('/​api/', limiter);

// 4. Request parsing
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));

// 5. Compression
app.use(compression());

// 6. Logging
if (process.env.NODE_ENV !== 'production') {
  app.use(morgan('dev'));
} else {
  app.use(morgan('combined'));
}

// 7. Static files (if needed)
app.use(express.static('public'));

// 8. Custom middleware
app.use(require('./​middleware/​requestId'));
app.use(require('./​middleware/​timing'));

// 9. Routes
app.use('/​api/​v1/​users', require('./​routes/​users'));
app.use('/​api/​v1/​posts', require('./​routes/​posts'));

// 10. 404 handler (after all routes)
app.use((req, res) => {
  res.status(404).json({ error: 'Route not found' });
});

// 11. Error handling (LAST)
app.use(require('./​middleware/​errorHandler'));

❌ Wrong Order:

// DON'T: Routes before security
app.use('/​api/​users', userRoutes); // Routes first
app.use(helmet()); // Security too late!

// DON'T: Error handler before routes
app.use(errorHandler); // Error handler first
app.use('/​api/​users', userRoutes); // Routes won't be caught

// DON'T: Parsing after routes
app.use('/​api/​users', userRoutes);
app.use(express.json()); // Too late to parse!

Custom Middleware Patterns

Request ID Middleware:

// middleware/​requestId.js
const { v4: uuidv4 } = require('uuid');

module.exports = function requestId(req, res, next) {
  req.id = req.headers['x-request-id'] || uuidv4();
  res.setHeader('X-Request-ID', req.id);
  next();
};

Request Timing Middleware:

// middleware/​timing.js
module.exports = function timing(req, res, next) {
  const start = Date.now();

  res.on('finish', () => {
    const duration = Date.now() - start;
    console.log(`${req.method} ${req.path} - ${duration}ms`);
  });

  next();
};

Authentication Middleware:

// middleware/​auth.js
const jwt = require('jsonwebtoken');
const AppError = require('../​utils/​AppError');

exports.authenticate = (req, res, next) => {
  const token = req.headers.authorization?.split(' ')[1];

  if (!token) {
    return next(new AppError('No token provided', 401));
  }

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded;
    next();
  } catch (error) {
    next(new AppError('Invalid token', 401));
  }
};

exports.authorize = (...roles) => {
  return (req, res, next) => {
    if (!req.user) {
      return next(new AppError('Not authenticated', 401));
    }

    if (!roles.includes(req.user.role)) {
      return next(new AppError('Insufficient permissions', 403));
    }

    next();
  };
};

Usage:

const { authenticate, authorize } = require('./​middleware/​auth');

// Public route
app.get('/​api/​posts', getPosts);

// Authenticated route
app.get('/​api/​profile', authenticate, getProfile);

// Role-based authorization
app.delete('/​api/​users/:id',
  authenticate,
  authorize('admin', 'moderator'),
  deleteUser
);

Async Middleware

✅ Correct Async Handling:

// utils/​catchAsync.js
module.exports = (fn) => {
  return (req, res, next) => {
    fn(req, res, next).catch(next);
  };
};

// Usage
const catchAsync = require('../​utils/​catchAsync');

app.get('/​users', catchAsync(async (req, res) => {
  const users = await User.find();
  res.json({ users });
}));

❌ Wrong: No Error Handling:

// DON'T: Async without catch
app.get('/​users', async (req, res) => {
  const users = await User.find(); // Unhandled rejection!
  res.json({ users });
});

Middleware Composition

Compose Multiple Middleware:

// middleware/​compose.js
const compose = (...middleware) => {
  return (req, res, next) => {
    let index = 0;

    const dispatch = (i) => {
      if (i >= middleware.length) return next();

      const fn = middleware[i];
      try {
        fn(req, res, () => dispatch(i + 1));
      } catch (err) {
        next(err);
      }
    };

    dispatch(0);
  };
};

// Usage
const adminOnly = compose(
  authenticate,
  authorize('admin'),
  validateRequest
);

app.delete('/​api/​users/:id', adminOnly, deleteUser);

Conditional Middleware:

// Apply middleware conditionally
const conditionalMiddleware = (condition, middleware) => {
  return (req, res, next) => {
    if (condition(req)) {
      return middleware(req, res, next);
    }
    next();
  };
};

// Only log in development
app.use(conditionalMiddleware(
  (req) => process.env.NODE_ENV === 'development',
  morgan('dev')
));

Structured Error Handling

Custom Error Classes

// utils/​AppError.js
class AppError extends Error {
  constructor(message, statusCode) {
    super(message);

    this.statusCode = statusCode;
    this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';
    this.isOperational = true;

    Error.captureStackTrace(this, this.constructor);
  }
}

module.exports = AppError;

Error Hierarchy:

// utils/​errors.js
class AppError extends Error {
  constructor(message, statusCode) {
    super(message);
    this.statusCode = statusCode;
    this.isOperational = true;
  }
}

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

class AuthenticationError extends AppError {
  constructor(message = 'Authentication required') {
    super(message, 401);
  }
}

class AuthorizationError extends AppError {
  constructor(message = 'Insufficient permissions') {
    super(message, 403);
  }
}

class NotFoundError extends AppError {
  constructor(resource = 'Resource') {
    super(`${resource} not found`, 404);
  }
}

class ConflictError extends AppError {
  constructor(message = 'Resource conflict') {
    super(message, 409);
  }
}

module.exports = {
  AppError,
  ValidationError,
  AuthenticationError,
  AuthorizationError,
  NotFoundError,
  ConflictError
};

Centralized Error Handler

// middleware/​errorHandler.js
const logger = require('../​config/​logger');

function errorHandler(err, req, res, next) {
  err.statusCode = err.statusCode || 500;
  err.status = err.status || 'error';

  // Log error
  logger.error({
    message: err.message,
    statusCode: err.statusCode,
    stack: err.stack,
    path: req.path,
    method: req.method,
    ip: req.ip,
    userId: req.user?.id
  });

  // Development: send full error
  if (process.env.NODE_ENV === 'development') {
    return res.status(err.statusCode).json({
      status: err.status,
      error: err,
      message: err.message,
      stack: err.stack
    });
  }

  // Production: sanitize errors
  if (err.isOperational) {
    // Operational, trusted error: send to client
    return res.status(err.statusCode).json({
      status: err.status,
      message: err.message,
      ...(err.errors && { errors: err.errors })
    });
  }

  // Programming or unknown error: don't leak details
  console.error('ERROR 💥', err);
  return res.status(500).json({
    status: 'error',
    message: 'Something went wrong'
  });
}

module.exports = errorHandler;

Handling Specific Error Types

// middleware/​errorHandler.js (extended)
function handleCastError(err) {
  const message = `Invalid ${err.path}: ${err.value}`;
  return new AppError(message, 400);
}

function handleDuplicateFields(err) {
  const field = Object.keys(err.keyValue)[0];
  const message = `Duplicate field value: ${field}. Please use another value`;
  return new AppError(message, 400);
}

function handleValidationError(err) {
  const errors = Object.values(err.errors).map(el => el.message);
  const message = `Invalid input data. ${errors.join('. ')}`;
  return new AppError(message, 400);
}

function handleJWTError() {
  return new AppError('Invalid token. Please log in again', 401);
}

function handleJWTExpiredError() {
  return new AppError('Your token has expired. Please log in again', 401);
}

module.exports = (err, req, res, next) => {
  let error = { ...err };
  error.message = err.message;

  // Mongoose bad ObjectId
  if (err.name === 'CastError') error = handleCastError(error);

  // Mongoose duplicate key
  if (err.code === 11000) error = handleDuplicateFields(error);

  // Mongoose validation error
  if (err.name === 'ValidationError') error = handleValidationError(error);

  // JWT errors
  if (err.name === 'JsonWebTokenError') error = handleJWTError();
  if (err.name === 'TokenExpiredError') error = handleJWTExpiredError();

  // Send response
  sendErrorResponse(error, req, res);
};

Async Error Handling

// utils/​catchAsync.js
const catchAsync = (fn) => {
  return (req, res, next) => {
    fn(req, res, next).catch(next);
  };
};

module.exports = catchAsync;

// Usage in controllers
const catchAsync = require('../​utils/​catchAsync');
const User = require('../​models/​User');
const { NotFoundError } = require('../​utils/​errors');

exports.getUser = catchAsync(async (req, res, next) => {
  const user = await User.findById(req.params.id);

  if (!user) {
    return next(new NotFoundError('User'));
  }

  res.json({ user });
});

exports.createUser = catchAsync(async (req, res, next) => {
  const user = await User.create(req.body);
  res.status(201).json({ user });
});

Unhandled Rejections

// server.js
const app = require('./​app');

const PORT = process.env.PORT || 3000;

const server = app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

// Handle unhandled promise rejections
process.on('unhandledRejection', (err) => {
  console.error('UNHANDLED REJECTION! 💥 Shutting down...');
  console.error(err.name, err.message);

  server.close(() => {
    process.exit(1);
  });
});

// Handle uncaught exceptions
process.on('uncaughtException', (err) => {
  console.error('UNCAUGHT EXCEPTION! 💥 Shutting down...');
  console.error(err.name, err.message);
  process.exit(1);
});

// Graceful shutdown
process.on('SIGTERM', () => {
  console.log('👋 SIGTERM RECEIVED. Shutting down gracefully');
  server.close(() => {
    console.log('💥 Process terminated!');
  });
});

Security Hardening

Helmet.js Configuration

// config/​security.js
const helmet = require('helmet');

const securityConfig = helmet({
  // Content Security Policy
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      scriptSrc: ["'self'"],
      imgSrc: ["'self'", "data:", "https:"],
      connectSrc: ["'self'"],
      fontSrc: ["'self'"],
      objectSrc: ["'none'"],
      mediaSrc: ["'self'"],
      frameSrc: ["'none'"],
    },
  },

  // Strict Transport Security
  hsts: {
    maxAge: 31536000, // 1 year
    includeSubDomains: true,
    preload: true
  },

  // X-Frame-Options
  frameguard: {
    action: 'deny'
  },

  // X-Content-Type-Options
  noSniff: true,

  // X-XSS-Protection
  xssFilter: true,

  // Referrer-Policy
  referrerPolicy: {
    policy: 'strict-origin-when-cross-origin'
  }
});

module.exports = securityConfig;

Usage:

// app.js
const securityConfig = require('./​config/​security');

app.use(securityConfig);

CORS Configuration

// config/​cors.js
const cors = require('cors');

const whitelist = process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'];

const corsOptions = {
  origin: function (origin, callback) {
    // Allow requests with no origin (mobile apps, Postman)
    if (!origin) return callback(null, true);

    if (whitelist.indexOf(origin) !== -1) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  credentials: true,
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
  allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
  exposedHeaders: ['X-Total-Count', 'X-Page-Number'],
  maxAge: 86400 // 24 hours
};

module.exports = cors(corsOptions);

Rate Limiting

// middleware/​rateLimiter.js
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
const redis = require('redis');

// Redis client for distributed rate limiting
const redisClient = redis.createClient({
  host: process.env.REDIS_HOST,
  port: process.env.REDIS_PORT
});

// General rate limiter
exports.generalLimiter = rateLimit({
  store: new RedisStore({
    client: redisClient,
    prefix: 'rl:general:'
  }),
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // limit each IP to 100 requests per windowMs
  message: 'Too many requests from this IP, please try again later',
  standardHeaders: true, // Return rate limit info in RateLimit-* headers
  legacyHeaders: false // Disable X-RateLimit-* headers
});

// Strict rate limiter for auth endpoints
exports.authLimiter = rateLimit({
  store: new RedisStore({
    client: redisClient,
    prefix: 'rl:auth:'
  }),
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 5, // limit each IP to 5 login attempts per windowMs
  message: 'Too many login attempts, please try again later',
  skipSuccessfulRequests: true // Don't count successful requests
});

// API key limiter (higher limits for authenticated users)
exports.apiKeyLimiter = rateLimit({
  windowMs: 60 * 60 * 1000, // 1 hour
  max: 1000,
  keyGenerator: (req) => req.headers['x-api-key'] || req.ip,
  skip: (req) => !req.headers['x-api-key']
});

Usage:

const { generalLimiter, authLimiter } = require('./​middleware/​rateLimiter');

// Apply to all routes
app.use('/​api/', generalLimiter);

// Strict limiting for auth
app.use('/​api/​auth/​login', authLimiter);
app.use('/​api/​auth/​register', authLimiter);

Input Validation and Sanitization

// middleware/​validation.js
const { body, param, query, validationResult } = require('express-validator');
const { ValidationError } = require('../​utils/​errors');

// Validation middleware
exports.validate = (req, res, next) => {
  const errors = validationResult(req);

  if (!errors.isEmpty()) {
    const extractedErrors = errors.array().map(err => ({
      field: err.param,
      message: err.msg,
      value: err.value
    }));

    return next(new ValidationError('Validation failed', extractedErrors));
  }

  next();
};

// User validation rules
exports.createUserRules = [
  body('email')
    .isEmail()
    .normalizeEmail()
    .withMessage('Must be a valid email'),
  body('password')
    .isLength({ min: 8 })
    .withMessage('Password must be at least 8 characters')
    .matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
    .withMessage('Password must contain uppercase, lowercase, and number'),
  body('name')
    .trim()
    .notEmpty()
    .withMessage('Name is required')
    .isLength({ max: 100 })
    .withMessage('Name too long')
    .escape(), // XSS protection
  body('age')
    .optional()
    .isInt({ min: 0, max: 150 })
    .withMessage('Age must be between 0 and 150')
];

exports.updateUserRules = [
  param('id')
    .isMongoId()
    .withMessage('Invalid user ID'),
  body('email')
    .optional()
    .isEmail()
    .normalizeEmail(),
  body('name')
    .optional()
    .trim()
    .notEmpty()
    .escape()
];

// Usage
const { createUserRules, validate } = require('./​middleware/​validation');

app.post('/​api/​users', createUserRules, validate, createUser);

SQL Injection Prevention

// DON'T: String concatenation
const query = `SELECT * FROM users WHERE email = '${req.body.email}'`; // Vulnerable!

// DO: Parameterized queries
const query = 'SELECT * FROM users WHERE email = ?';
connection.query(query, [req.body.email], (err, results) => {
  // Safe from SQL injection
});

// DO: ORM/​Query Builder
const user = await User.findOne({ email: req.body.email }); // Mongoose
const user = await db('users').where('email', req.body.email).first(); // Knex

XSS Protection

// Install: npm install xss-clean
const xss = require('xss-clean');

// Apply XSS sanitization
app.use(xss());

// Additional: HTML escaping in templates
const escapeHtml = (unsafe) => {
  return unsafe
    .replace(/​&/​g, "&")
    .replace(/</​g, "&lt;")
    .replace(/>/​g, "&gt;")
    .replace(/"/​g, "&quot;")
    .replace(/'/​g, "&#039;");
};

Environment Variable Security

// config/​index.js
require('dotenv').config();

const requiredEnvVars = [
  'NODE_ENV',
  'PORT',
  'DATABASE_URL',
  'JWT_SECRET',
  'REDIS_HOST'
];

// Validate required environment variables
requiredEnvVars.forEach((envVar) => {
  if (!process.env[envVar]) {
    throw new Error(`Missing required environment variable: ${envVar}`);
  }
});

// Validate JWT_SECRET strength
if (process.env.JWT_SECRET.length < 32) {
  throw new Error('JWT_SECRET must be at least 32 characters');
}

module.exports = {
  env: process.env.NODE_ENV,
  port: parseInt(process.env.PORT, 10),
  database: {
    url: process.env.DATABASE_URL
  },
  jwt: {
    secret: process.env.JWT_SECRET,
    expiresIn: process.env.JWT_EXPIRES_IN || '7d'
  },
  redis: {
    host: process.env.REDIS_HOST,
    port: parseInt(process.env.REDIS_PORT, 10) || 6379
  }
};

Testing with Supertest

Test Setup

// tests/​setup.js
const mongoose = require('mongoose');
const { MongoMemoryServer } = require('mongodb-memory-server');

let mongoServer;

// Setup before all tests
beforeAll(async () => {
  mongoServer = await MongoMemoryServer.create();
  const mongoUri = mongoServer.getUri();

  await mongoose.connect(mongoUri);
});

// Cleanup after each test
afterEach(async () => {
  const collections = mongoose.connection.collections;

  for (const key in collections) {
    await collections[key].deleteMany();
  }
});

// Teardown after all tests
afterAll(async () => {
  await mongoose.disconnect();
  await mongoServer.stop();
});

Integration Testing

// tests/​integration/​users.test.js
const request = require('supertest');
const app = require('../​../​src/​app');
const User = require('../​../​src/​models/​User');

describe('User API', () => {
  describe('POST /​api/​users', () => {
    it('should create a new user', async () => {
      const userData = {
        email: '[email protected]',
        name: 'Test User',
        password: 'Password123'
      };

      const response = await request(app)
        .post('/​api/​users')
        .send(userData)
        .expect('Content-Type', /​json/)
        .expect(201);

      expect(response.body).toHaveProperty('user');
      expect(response.body.user.email).toBe(userData.email);
      expect(response.body.user).not.toHaveProperty('password');
    });

    it('should return 400 for invalid email', async () => {
      const response = await request(app)
        .post('/​api/​users')
        .send({
          email: 'invalid-email',
          name: 'Test User',
          password: 'Password123'
        })
        .expect(400);

      expect(response.body).toHaveProperty('errors');
    });

    it('should return 409 for duplicate email', async () => {
      const userData = {
        email: '[email protected]',
        name: 'Test User',
        password: 'Password123'
      };

      // Create first user
      await User.create(userData);

      // Try to create duplicate
      const response = await request(app)
        .post('/​api/​users')
        .send(userData)
        .expect(409);

      expect(response.body.message).toMatch(/​duplicate/​i);
    });
  });

  describe('GET /​api/​users/:id', () => {
    it('should get user by ID', async () => {
      const user = await User.create({
        email: '[email protected]',
        name: 'Get User',
        password: 'Password123'
      });

      const response = await request(app)
        .get(`/​api/​users/​${user._id}`)
        .expect(200);

      expect(response.body.user._id).toBe(user._id.toString());
    });

    it('should return 404 for non-existent user', async () => {
      const fakeId = '507f1f77bcf86cd799439011';

      await request(app)
        .get(`/​api/​users/​${fakeId}`)
        .expect(404);
    });
  });

  describe('PUT /​api/​users/:id', () => {
    it('should update user', async () => {
      const user = await User.create({
        email: '[email protected]',
        name: 'Update User',
        password: 'Password123'
      });

      const response = await request(app)
        .put(`/​api/​users/​${user._id}`)
        .send({ name: 'Updated Name' })
        .expect(200);

      expect(response.body.user.name).toBe('Updated Name');
    });
  });

  describe('DELETE /​api/​users/:id', () => {
    it('should delete user', async () => {
      const user = await User.create({
        email: '[email protected]',
        name: 'Delete User',
        password: 'Password123'
      });

      await request(app)
        .delete(`/​api/​users/​${user._id}`)
        .expect(204);

      const deletedUser = await User.findById(user._id);
      expect(deletedUser).toBeNull();
    });
  });
});

Authentication Testing

// tests/​integration/​auth.test.js
const request = require('supertest');
const app = require('../​../​src/​app');
const User = require('../​../​src/​models/​User');

describe('Authentication', () => {
  let authToken;
  let testUser;

  beforeEach(async () => {
    // Create test user
    testUser = await User.create({
      email: '[email protected]',
      name: 'Auth User',
      password: 'Password123'
    });

    // Login to get token
    const response = await request(app)
      .post('/​api/​auth/​login')
      .send({
        email: '[email protected]',
        password: 'Password123'
      });

    authToken = response.body.token;
  });

  describe('POST /​api/​auth/​login', () => {
    it('should login with valid credentials', async () => {
      const response = await request(app)
        .post('/​api/​auth/​login')
        .send({
          email: '[email protected]',
          password: 'Password123'
        })
        .expect(200);

      expect(response.body).toHaveProperty('token');
      expect(response.body).toHaveProperty('user');
    });

    it('should reject invalid credentials', async () => {
      await request(app)
        .post('/​api/​auth/​login')
        .send({
          email: '[email protected]',
          password: 'WrongPassword'
        })
        .expect(401);
    });
  });

  describe('GET /​api/​auth/​me', () => {
    it('should get current user with valid token', async () => {
      const response = await request(app)
        .get('/​api/​auth/​me')
        .set('Authorization', `Bearer ${authToken}`)
        .expect(200);

      expect(response.body.user.email).toBe('[email protected]');
    });

    it('should reject request without token', async () => {
      await request(app)
        .get('/​api/​auth/​me')
        .expect(401);
    });

    it('should reject request with invalid token', async () => {
      await request(app)
        .get('/​api/​auth/​me')
        .set('Authorization', 'Bearer invalid-token')
        .expect(401);
    });
  });
});

Test Factories and Fixtures

// tests/​factories/​userFactory.js
const User = require('../​../​src/​models/​User');

let userCount = 0;

exports.createUser = async (overrides = {}) => {
  userCount++;

  const defaultData = {
    email: `user${userCount}@example.com`,
    name: `User ${userCount}`,
    password: 'Password123'
  };

  return User.create({ ...defaultData, ...overrides });
};

exports.createUsers = async (count, overrides = {}) => {
  const users = [];
  for (let i = 0; i < count; i++) {
    users.push(await exports.createUser(overrides));
  }
  return users;
};

Usage:

const { createUser, createUsers } = require('../​factories/​userFactory');

describe('User operations', () => {
  it('should list all users', async () => {
    await createUsers(5);

    const response = await request(app)
      .get('/​api/​users')
      .expect(200);

    expect(response.body.users).toHaveLength(5);
  });

  it('should create admin user', async () => {
    const admin = await createUser({ role: 'admin' });
    expect(admin.role).toBe('admin');
  });
});

Test Coverage

// package.json
{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage",
    "test:unit": "jest tests/​unit",
    "test:integration": "jest tests/​integration"
  },
  "jest": {
    "testEnvironment": "node",
    "coveragePathIgnorePatterns": ["/​node_modules/"],
    "collectCoverageFrom": [
      "src/​**/​*.js",
      "!src/​tests/​**"
    ],
    "coverageThreshold": {
      "global": {
        "branches": 80,
        "functions": 80,
        "lines": 80,
        "statements": 80
      }
    }
  }
}

Production Operations

Environment Configuration

// config/​index.js
require('dotenv').config();

const config = {
  // Environment
  env: process.env.NODE_ENV || 'development',
  port: parseInt(process.env.PORT, 10) || 3000,

  // Database
  database: {
    url: process.env.DATABASE_URL,
    poolMin: parseInt(process.env.DB_POOL_MIN, 10) || 2,
    poolMax: parseInt(process.env.DB_POOL_MAX, 10) || 10
  },

  // Redis
  redis: {
    host: process.env.REDIS_HOST || 'localhost',
    port: parseInt(process.env.REDIS_PORT, 10) || 6379,
    password: process.env.REDIS_PASSWORD
  },

  // JWT
  jwt: {
    secret: process.env.JWT_SECRET,
    expiresIn: process.env.JWT_EXPIRES_IN || '7d',
    refreshExpiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '30d'
  },

  // CORS
  cors: {
    origins: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000']
  },

  // Rate Limiting
  rateLimit: {
    windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS, 10) || 900000,
    max: parseInt(process.env.RATE_LIMIT_MAX, 10) || 100
  },

  // Logging
  logging: {
    level: process.env.LOG_LEVEL || 'info',
    file: process.env.LOG_FILE || 'logs/​app.log'
  }
};

// Validate required configuration
const requiredConfig = [
  'database.url',
  'jwt.secret'
];

requiredConfig.forEach(key => {
  const value = key.split('.').reduce((obj, k) => obj?.[k], config);
  if (!value) {
    throw new Error(`Missing required configuration: ${key}`);
  }
});

module.exports = config;

.env.example:

# Environment
NODE_ENV=production
PORT=3000

# Database
DATABASE_URL=mongodb://localhost:27017/​myapp
DB_POOL_MIN=2
DB_POOL_MAX=10

# Redis
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=

# JWT
JWT_SECRET=your-super-secret-jwt-key-min-32-chars
JWT_EXPIRES_IN=7d
JWT_REFRESH_EXPIRES_IN=30d

# CORS
ALLOWED_ORIGINS=https://example.com,https://www.example.com

# Rate Limiting
RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX=100

# Logging
LOG_LEVEL=info
LOG_FILE=logs/​app.log

Structured Logging

// config/​logger.js
const winston = require('winston');
const path = require('path');

const logLevels = {
  error: 0,
  warn: 1,
  info: 2,
  http: 3,
  debug: 4
};

const logColors = {
  error: 'red',
  warn: 'yellow',
  info: 'green',
  http: 'magenta',
  debug: 'blue'
};

winston.addColors(logColors);

const format = winston.format.combine(
  winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss:ms' }),
  winston.format.errors({ stack: true }),
  winston.format.splat(),
  winston.format.json()
);

const transports = [
  // Error logs
  new winston.transports.File({
    filename: path.join('logs', 'error.log'),
    level: 'error',
    maxsize: 5242880, // 5MB
    maxFiles: 5
  }),

  // Combined logs
  new winston.transports.File({
    filename: path.join('logs', 'combined.log'),
    maxsize: 5242880,
    maxFiles: 5
  })
];

// Console transport in development
if (process.env.NODE_ENV !== 'production') {
  transports.push(
    new winston.transports.Console({
      format: winston.format.combine(
        winston.format.colorize({ all: true }),
        winston.format.printf(
          (info) => `${info.timestamp} ${info.level}: ${info.message}`
        )
      )
    })
  );
}

const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  levels: logLevels,
  format,
  transports
});

module.exports = logger;

Usage:

const logger = require('./​config/​logger');

logger.info('Server started', { port: 3000 });
logger.error('Database connection failed', { error: err.message });
logger.debug('User data', { userId: user.id, email: user.email });

Request Logging Middleware:

// middleware/​requestLogger.js
const logger = require('../​config/​logger');

module.exports = (req, res, next) => {
  const start = Date.now();

  res.on('finish', () => {
    const duration = Date.now() - start;

    logger.http('Request completed', {
      method: req.method,
      url: req.url,
      statusCode: res.statusCode,
      duration: `${duration}ms`,
      ip: req.ip,
      userAgent: req.get('user-agent'),
      userId: req.user?.id
    });
  });

  next();
};

Health Check Endpoints

// routes/​health.js
const express = require('express');
const router = express.Router();
const mongoose = require('mongoose');
const redis = require('redis');

const redisClient = redis.createClient();

// Basic health check
router.get('/​health', (req, res) => {
  res.json({
    status: 'ok',
    uptime: process.uptime(),
    timestamp: new Date().toISOString()
  });
});

// Detailed health check
router.get('/​health/​detailed', async (req, res) => {
  const health = {
    status: 'ok',
    timestamp: new Date().toISOString(),
    uptime: process.uptime(),
    services: {}
  };

  // Check MongoDB
  try {
    const mongoState = mongoose.connection.readyState;
    health.services.mongodb = {
      status: mongoState === 1 ? 'connected' : 'disconnected',
      state: mongoState
    };
  } catch (error) {
    health.services.mongodb = {
      status: 'error',
      error: error.message
    };
    health.status = 'degraded';
  }

  // Check Redis
  try {
    await redisClient.ping();
    health.services.redis = {
      status: 'connected'
    };
  } catch (error) {
    health.services.redis = {
      status: 'error',
      error: error.message
    };
    health.status = 'degraded';
  }

  // Memory usage
  const memUsage = process.memoryUsage();
  health.memory = {
    rss: `${Math.round(memUsage.rss / 1024 / 1024)}MB`,
    heapUsed: `${Math.round(memUsage.heapUsed / 1024 / 1024)}MB`,
    heapTotal: `${Math.round(memUsage.heapTotal / 1024 / 1024)}MB`
  };

  const statusCode = health.status === 'ok' ? 200 : 503;
  res.status(statusCode).json(health);
});

// Readiness check (Kubernetes)
router.get('/​ready', async (req, res) => {
  try {
    // Check if app can serve requests
    await mongoose.connection.db.admin().ping();
    res.status(200).json({ status: 'ready' });
  } catch (error) {
    res.status(503).json({ status: 'not ready', error: error.message });
  }
});

// Liveness check (Kubernetes)
router.get('/​live', (req, res) => {
  res.status(200).json({ status: 'alive' });
});

module.exports = router;

Graceful Shutdown

// server.js
const app = require('./​app');
const logger = require('./​config/​logger');
const mongoose = require('./​config/​database');
const redis = require('./​config/​redis');

const PORT = process.env.PORT || 3000;

const server = app.listen(PORT, () => {
  logger.info(`Server running on port ${PORT}`);
});

// Graceful shutdown function
async function gracefulShutdown(signal) {
  logger.info(`${signal} received, starting graceful shutdown`);

  // Stop accepting new connections
  server.close(async () => {
    logger.info('HTTP server closed');

    try {
      // Close database connections
      await mongoose.connection.close(false);
      logger.info('MongoDB connection closed');

      // Close Redis connection
      await redis.quit();
      logger.info('Redis connection closed');

      // Close any other resources
      // await closeOtherResources();

      logger.info('Graceful shutdown completed');
      process.exit(0);
    } catch (error) {
      logger.error('Error during shutdown', { error: error.message });
      process.exit(1);
    }
  });

  // Force shutdown after timeout
  setTimeout(() => {
    logger.error('Forcing shutdown after timeout');
    process.exit(1);
  }, 30000); // 30 seconds
}

// Handle termination signals
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));

// Handle uncaught errors
process.on('uncaughtException', (error) => {
  logger.error('Uncaught exception', { error: error.message, stack: error.stack });
  gracefulShutdown('uncaughtException');
});

process.on('unhandledRejection', (reason, promise) => {
  logger.error('Unhandled rejection', { reason, promise });
  gracefulShutdown('unhandledRejection');
});

module.exports = server;

PM2 Clustering

// ecosystem.config.js
module.exports = {
  apps: [{
    name: 'express-api',
    script: './​src/​server.js',

    // Clustering
    instances: 'max', // Use all CPU cores
    exec_mode: 'cluster',

    // Environment variables
    env: {
      NODE_ENV: 'development',
      PORT: 3000
    },
    env_production: {
      NODE_ENV: 'production',
      PORT: 8080
    },

    // Restart policies
    autorestart: true,
    max_restarts: 10,
    min_uptime: '10s',
    max_memory_restart: '500M',

    // Graceful shutdown
    kill_timeout: 5000,
    wait_ready: true,
    listen_timeout: 10000,

    // Logging
    error_file: './​logs/​pm2-error.log',
    out_file: './​logs/​pm2-out.log',
    log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
    merge_logs: true,

    // Monitoring
    instance_var: 'INSTANCE_ID',

    // Watch (development only)
    watch: false
  }],

  // Deploy configuration
  deploy: {
    production: {
      user: 'deploy',
      host: 'production.example.com',
      ref: 'origin/​main',
      repo: '[email protected]:username/​repo.git',
      path: '/​var/​www/​production',
      'post-deploy': 'npm install && pm2 reload ecosystem.config.js --env production'
    }
  }
};

PM2 Commands:

# Start cluster
pm2 start ecosystem.config.js --env production

# Zero-downtime reload
pm2 reload express-api

# Monitor
pm2 monit

# View logs
pm2 logs express-api

# Scale instances
pm2 scale express-api 4

# Stop
pm2 stop express-api

# Restart
pm2 restart express-api

# Delete
pm2 delete express-api

# Save process list
pm2 save

# Startup script
pm2 startup

# Deploy
pm2 deploy production

Development Workflow

Nodemon Configuration

{
  "watch": ["src"],
  "ext": "js,json",
  "ignore": [
    "src/​**/​*.test.js",
    "src/​**/​*.spec.js",
    "node_modules/​**/​*",
    "logs/​**/​*"
  ],
  "exec": "node src/​server.js",
  "env": {
    "NODE_ENV": "development",
    "PORT": "3000"
  },
  "delay": 1000,
  "verbose": false,
  "restartable": "rs",
  "signal": "SIGTERM"
}

Package.json Scripts

{
  "scripts": {
    "dev": "nodemon src/​server.js",
    "dev:debug": "nodemon --inspect src/​server.js",
    "start": "node src/​server.js",
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage",
    "lint": "eslint src/​**/​*.js",
    "lint:fix": "eslint src/​**/​*.js --fix",
    "format": "prettier --write \"src/​**/​*.js\"",
    "prod": "pm2 start ecosystem.config.js --env production",
    "reload": "pm2 reload express-api",
    "stop": "pm2 stop express-api",
    "logs": "pm2 logs express-api"
  }
}

Decision Trees

Middleware Selection

Need middleware?
├─ Security?
│  ├─ Headers → helmet
│  ├─ CORS → cors
│  ├─ Rate limiting → express-rate-limit
│  └─ Input validation → express-validator
├─ Parsing?
│  ├─ JSON → express.json()
│  ├─ Form data → express.urlencoded()
│  └─ Multipart → multer
├─ Logging?
│  ├─ Development → morgan('dev')
│  └─ Production → winston + morgan('combined')
├─ Compression?
│  └─ Response compression → compression()
└─ Authentication?
   ├─ Session-based → express-session + connect-redis
   └─ Token-based → jsonwebtoken

Error Handling Strategy

Error occurred?
├─ Operational error? (Known error)
│  ├─ Validation error → 400 with details
│  ├─ Authentication error → 401
│  ├─ Authorization error → 403
│  ├─ Not found error → 404
│  └─ Conflict error → 409
├─ Programming error? (Bug)
│  ├─ Development → Send full error + stack
│  └─ Production → Log error, send generic message
└─ External service error?
   ├─ Retry → Exponential backoff
   └─ Circuit breaker → Fail fast

Testing Approach

What to test?
├─ API endpoints?
│  └─ Integration tests → Supertest
├─ Business logic?
│  └─ Unit tests → Jest
├─ Database operations?
│  └─ Integration tests → MongoMemoryServer
├─ Authentication?
│  └─ Integration tests → Test token flow
└─ Error handling?
   └─ Unit + Integration tests → Test error cases

Deployment Pattern

Deployment target?
├─ Local development?
│  └─ Nodemon
├─ Single server?
│  ├─ Small app → node server.js
│  └─ Production → PM2 (single instance)
├─ Multi-core server?
│  └─ PM2 cluster mode
├─ Container?
│  ├─ Single container → Docker + node
│  └─ Orchestrated → Docker + Kubernetes
└─ Serverless?
   └─ AWS Lambda + API Gateway

Common Problems & Solutions

Problem 1: Port Already in Use

Symptoms:

Error: listen EADDRINUSE: address already in use :::3000

Solution:

# Find and kill process on port
lsof -ti:3000 | xargs kill -9

# Or use different port
PORT=3001 npm run dev

# Or add cleanup script
{
  "scripts": {
    "predev": "kill-port 3000 || true",
    "dev": "nodemon server.js"
  }
}

Problem 2: Middleware Order Issues

Symptom: Routes not working, errors not caught, CORS failures

Solution: Follow correct middleware order:

  1. Security (helmet, cors)
  2. Rate limiting
  3. Parsing (json, urlencoded)
  4. Compression
  5. Logging
  6. Custom middleware
  7. Routes
  8. 404 handler
  9. Error handler (last!)

Problem 3: Unhandled Promise Rejections

Symptom: UnhandledPromiseRejectionWarning

Solution:

// Use catchAsync wrapper
const catchAsync = require('./​utils/​catchAsync');

app.get('/​users', catchAsync(async (req, res) => {
  const users = await User.find();
  res.json({ users });
}));

// Or handle at process level
process.on('unhandledRejection', (err) => {
  console.error('UNHANDLED REJECTION!', err);
  server.close(() => process.exit(1));
});

Problem 4: Sessions Not Working in Cluster Mode

Symptom: User logged in but subsequent requests show logged out

Solution: Use Redis session store

const session = require('express-session');
const RedisStore = require('connect-redis').default;
const redis = require('redis');

const redisClient = redis.createClient();

app.use(session({
  store: new RedisStore({ client: redisClient }),
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false
}));

Problem 5: Memory Leaks

Symptoms: Memory usage grows over time, server crashes

Solution:

# Monitor memory with PM2
pm2 start server.js --max-memory-restart 500M

# Profile with Node
node --inspect server.js
# Then use Chrome DevTools

# Use clinic.js
npm install -g clinic
clinic doctor -- node server.js

Anti-Patterns

❌ Don't: Mix Concerns

// WRONG: Business logic in routes
app.post('/​users', async (req, res) => {
  const user = new User(req.body);
  user.password = await bcrypt.hash(req.body.password, 10);
  await user.save();
  const token = jwt.sign({ id: user.id }, process.env.JWT_SECRET);
  res.json({ user, token });
});

✅ Do: Separate Concerns:

// CORRECT: Use controllers and services
app.post('/​users',
  validate(createUserRules),
  userController.create
);

// controller
exports.create = catchAsync(async (req, res) => {
  const user = await userService.createUser(req.body);
  const token = authService.generateToken(user);
  res.status(201).json({ user, token });
});

❌ Don't: Sync Operations

// WRONG
const data = fs.readFileSync('./​data.json');

✅ Do: Async Operations:

// CORRECT
const data = await fs.promises.readFile('./​data.json');

❌ Don't: Trust User Input

// WRONG
app.post('/​users', (req, res) => {
  User.create(req.body); // Dangerous!
});

✅ Do: Validate and Sanitize:

// CORRECT
app.post('/​users',
  validate(createUserRules),
  userController.create
);

Quick Reference

Essential Middleware Stack

const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const compression = require('compression');
const morgan = require('morgan');
const rateLimit = require('express-rate-limit');

const app = express();

// Minimal production stack
app.use(helmet());
app.use(cors());
app.use(rateLimit({ windowMs: 15 * 60 * 1000, max: 100 }));
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));
app.use(compression());
app.use(morgan('combined'));

// Routes
app.use('/​api/​v1', require('./​routes'));

// Error handler
app.use(require('./​middleware/​errorHandler'));

Essential Commands

# Development
npm run dev                    # Start with nodemon
npm test                       # Run tests
npm run test:watch             # Watch mode
npm run lint                   # Lint code

# Production
npm start                      # Start production
pm2 start ecosystem.config.js  # Start with PM2
pm2 reload app                 # Zero-downtime reload
pm2 logs app                   # View logs
pm2 monit                      # Monitor

# Testing
npm test                       # All tests
npm run test:unit              # Unit tests
npm run test:integration       # Integration tests
npm run test:coverage          # Coverage report

Related Skills

  • nodejs-backend - Node.js backend development patterns
  • fastify-production - Fastify framework (performance-focused alternative)
  • typescript-core - TypeScript with Express
  • docker-containerization - Containerized Express deployment
  • systematic-debugging - Advanced debugging techniques

Progressive Disclosure

For detailed implementation guides, see:


Version: Express 4.x, PM2 5.x, Node.js 18+ Last Updated: December 2025 License: MIT

More skills from mattnigh

spring-framework-patterns logo
mattnigh/skills_collection

spring-framework-patterns

Comprehensive Spring Framework and Spring Boot best practices including dependency injection patterns, bean lifecycle and scopes, REST API development, Spring Data JPA, service layer design, Spring Security, testing strategies, caching, AOP, async processing, error handling, and common anti-patte...

24 0
View

TypeScript厳格モードによる型安全性設計を専門とするスキル。 📖 参照書籍: - 『Effective TypeScript』(Dan Vanderkam): 型設計 📚 リソース参照: - `resources/Level1_basics.md`: レベル1の基礎ガイド - `resources/Level2_intermediate.md`: レベル2の実務ガイド - `resources/Level3_advanced.md`: レベル3の応用ガイド - `resources/Level4_expert.md`: レベル4の専門ガイド - `resources/di...

24 0
View
deploying-cloud-k8s logo
mattnigh/skills_collection

deploying-cloud-k8s

Deploys applications to cloud Kubernetes (AKS/GKE/DOKS) with CI/CD pipelines. Use when deploying to production, setting up GitHub Actions, troubleshooting deployments. Covers build-time vs runtime vars, architecture matching, and battle-tested debugging.

24 0
View
moai-alfred-agent-guide logo
mattnigh/skills_collection

moai-alfred-agent-guide

19-agent team structure, decision trees for agent selection, Haiku vs Sonnet model selection, and agent collaboration principles. Use when deciding which sub-agent to invoke, understanding team responsibilities, or learning multi-agent orchestration.

24 0
View
mermaid-diagram-generator logo
mattnigh/skills_collection

mermaid-diagram-generator

Converts architecture descriptions, module specs, or workflow docs into Mermaid diagrams. Use when visualizing brick module relationships, workflows (DDD, investigation), or system architecture. Supports: flowcharts, sequence diagrams, class diagrams, state machines, entity relationship diagrams,...

24 0
View
pre-flight-check logo
mattnigh/skills_collection

pre-flight-check

INVOKE FIRST before any code work. Validates git workflow (branch, issue, worklog) and checks approach. Use at START of every task and END before completing. Prevents skipped steps.

24 0
View
browser-dev-tools logo
mattnigh/skills_collection

browser-dev-tools

This skill should be used when working with browser-rendered artifacts (i.e. Bun, React/React Native) to proactively validate that development work on localhost is being built appropriately and to support debugging browser-rendered content. Use this skill after making frontend changes to verify t...

24 0
View
lead-dev logo
mattnigh/skills_collection

lead-dev

Lead Développeur - Coordination technique opérationnelle, code review, mentoring et livraison. Pair de web-dev-process au niveau OPÉRATIONS.

24 0
View
worker-monitor logo
mattnigh/skills_collection

worker-monitor

Monitor Docker workers, RQ queue health, and auto-scale workers (max 4). Use when: checking job progress, monitoring queue depth, scaling workers up/down, diagnosing slow processing, or waiting for jobs to complete. Referenced by data-quality skill during reprocessing.

24 0
View
quality-advisor logo
mattnigh/skills_collection

quality-advisor

Proactive quality guidance system that monitors artifact creation and provides real-time feedback on documentation quality

24 0
View
bump-version logo
mattnigh/skills_collection

bump-version

This skill should be used when the user wants to bump the version number in the workspace. It updates versions across all pyproject.toml files (root, tde, and tda packages) and the CHANGELOG.md to keep them synchronized.

24 0
View

Popular AI tools

Kaiber logo
Video

Kaiber

Generate, edit, and beat-sync AI video with leading models in one workspace.

Paid
View
Vimcal logo
Productivity

Vimcal

The world's fastest calendar for remote work

Free
View

Transform Your Design with AI Designer by ImgCreator.ai

Freemium
View
Akool AI logo
Content & writing

Akool AI

Revolutionizing Video Production with AI-Powered Creativity

Paid
View

Extend an image past the frame and let AI fill the new aspect ratio.

Freemium
View
StarByFace logo
Security

StarByFace

Discover your celebrity doppelgänger with StarByFace!

Free
View
C

ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.

Freemium
View
Opus Clip logo
Coding & apps

Opus Clip

Opus.ai: Revolutionize Your Web Experience

Free
View