npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

express-response-engine

v1.4.2

Published

A framework-agnostic, TypeScript-ready response and error handling engine for Express, featuring global middleware, custom ApiError helpers, validation formatting, response encryption, logging hooks, and request ID support.

Readme

express-response-engine

express-response-engine is a framework-agnostic, zero-runtime-dependency, TypeScript-ready response and error handling engine for Express applications. It provides global middlewares, standard API response wrappers, validation formatting, response encryption, logging hooks, and request ID support.

Features

  • Global Error Middleware: Catch all errors and format them consistently.
  • Response Interceptor: Injects helper methods onto the res object.
  • Async Handler Wrapper: Wraps async controller routes to automatically forward errors to Express error handlers (supports Express 4 & 5).
  • Custom ApiError Class: Easily throw descriptive errors (e.g. throw ApiError.notFound('User not found')).
  • Unified Responses: Clean shapes for success and error bodies.
  • Validation Formatter: Auto-formats validation errors from Zod, Joi, and Express Validator.
  • Optional AES-256-GCM Encryption: Secure sensitive success payloads automatically.
  • Request ID Support: Generates or forwards unique request tracing IDs.
  • Logging Hooks: Attach Winston, Pino, or any logging callback.
  • TypeScript Support: Full autocomplete and type-safety with zero configuration.
  • CLI Server Generator: Scaffold a ready-to-run Express server with one command.
  • CLI Module Generator: Scaffold Router → Controller → Service → Validation in one command.

Installation

npm install express-response-engine

Quick Start

1. Basic Integration

Import and set up the response interceptor and global error middleware in your Express application:

const express = require('express');
const asyncHandler = require('express-response-engine');
const { responseInterceptor, errorInterceptor } = require('express-response-engine');

const app = express();

app.use(express.json());

// 1. Initialize the Response Interceptor (attaches res.success, res.badRequest, etc.)
app.use(responseInterceptor());

// 2. Wrap your route controllers with asyncHandler
app.get(
  '/users',
  asyncHandler(async (req, res) => {
    const users = [{ id: 1, name: 'Alice' }];
    
    // Sends standard 200 Success Response
    return res.success(users, 'Users retrieved successfully');
  })
);

// 3. Register the Global Error Interceptor at the bottom of your middleware chain
app.use(errorInterceptor());

app.listen(3000);

2. TypeScript Setup

If you are using TypeScript, express-response-engine automatically augments Express Request and Response interfaces. Just import the library once in your entry point:

import express from 'express';
import asyncHandler, { responseInterceptor, errorInterceptor, ApiError } from 'express-response-engine';

const app = express();
app.use(responseInterceptor());

app.get('/users', asyncHandler(async (req, res) => {
  // `res.success` is fully typed and autocompletes!
  return res.success({ hello: 'world' }); 
}));

API Response Formatting

Success Response Format

Success payloads use the keys specified (defaults to success, statusCode, message, data):

{
  "success": true,
  "statusCode": 200,
  "message": "Users retrieved successfully",
  "data": [
    { "id": 1, "name": "Alice" }
  ]
}

Error Response Format

Error responses structure details inside the errors property (defaults to success, statusCode, message and errors):

{
  "success": false,
  "statusCode": 404,
  "message": "User not found",
  "errors": null
}

Throw Helpers and Custom ApiError

Instead of passing errors to next(), you can throw a custom ApiError directly from your controllers. The global errorInterceptor will automatically intercept it and return the correct HTTP status code.

const { ApiError } = require('express-response-engine');

// Inside a controller:
throw ApiError.notFound('User not found');

// Or with structured details:
throw ApiError.badRequest('Missing mandatory parameters', { missing: ['email', 'password'] });

Supported Throw Helpers:

  • ApiError.badRequest(message, errors) (400)
  • ApiError.unauthorized(message) (401)
  • ApiError.forbidden(message) (403)
  • ApiError.notFound(message) (404)
  • ApiError.conflict(message) (409)
  • ApiError.validationError(errors, message) (422)
  • ApiError.tooManyRequests(message) (429)
  • ApiError.internal(message, errors) (500)

Response Interceptor Helpers

The responseInterceptor() attaches these quick-access helper methods to the res object:

Success Helpers:

  • res.success(data, message) - 200 OK
  • res.created(data, message) - 201 Created
  • res.updated(data, message) - 200 OK
  • res.deleted(data, message) - 200 OK

Error Helpers (Manual response without throwing):

  • res.badRequest(message, errors) - 400 Bad Request
  • res.unauthorized(message) - 401 Unauthorized
  • res.forbidden(message) - 403 Forbidden
  • res.notFound(message) - 404 Not Found
  • res.conflict(message) - 409 Conflict
  • res.validationError(errors, message) - 422 Unprocessable Entity
  • res.tooManyRequests(message) - 429 Too Many Requests
  • res.internalServerError(message, errors) - 500 Internal Server Error

Config Customization

You can customize the structure of your JSON responses globally using the configure function, or locally in each middleware instance:

Global Customization

const { configure } = require('express-response-engine');

configure({
  successKey: 'ok',
  statusCodeKey: 'status_code',
  dataKey: 'payload',
  errorKey: 'err_details',
  requestIdHeader: 'x-correlation-id'
});

Local Middleware Customization

Passing parameters to responseInterceptor() or errorInterceptor() overrides the global defaults for that specific router/app:

app.use(responseInterceptor({
  successKey: 'ok',
  dataKey: 'result'
}));

Automatic Validation Error Formatting

If a validation error is thrown from popular validation libraries, the errorInterceptor automatically normalizes it:

1. Zod

Formats thrown ZodError lists into flat structures:

{
  "success": false,
  "statusCode": 422,
  "message": "Validation Error",
  "errors": [
    {
      "field": "body.email",
      "message": "Invalid email address",
      "rule": "invalid_string"
    }
  ]
}

2. Joi

Formats thrown Joi ValidationError lists:

{
  "success": false,
  "statusCode": 422,
  "message": "Validation Error",
  "errors": [
    {
      "field": "body.password",
      "message": "Password must be at least 8 characters long",
      "rule": "string.min"
    }
  ]
}

3. Express Validator

Normalizes validator array formats:

{
  "success": false,
  "statusCode": 422,
  "message": "Validation Error",
  "errors": [
    {
      "field": "username",
      "message": "Username must be alphanumeric",
      "value": "usr@name!"
    }
  ]
}

Custom Errors: CustomError vs ApiError

express-response-engine provides two distinct error classes to handle different project architectures:

1. ApiError (Modern API standard)

Designed for clean, status-driven HTTP REST APIs. It provides semantic static helpers:

const { ApiError } = require('express-response-engine');

// Throw directly from routes:
throw ApiError.notFound('Resource does not exist');
throw ApiError.badRequest('Invalid fields', { email: 'Email required' });

2. CustomError (Legacy drop-in compatibility)

Specifically designed to match legacy backend error patterns:

const { CustomError } = require('express-response-engine');

// Signature: CustomError(message, status, statusText, data)
throw new CustomError(
  'Failed to authenticate with smart credit API', 
  400, 
  'LOGIN_FAILED', 
  { detail: 'API timeout' }
);

The global errorInterceptor detects the numeric .status property, extracts the custom string code from .statusCode (e.g. 'LOGIN_FAILED'), and outputs it in the payload.


Internal Mechanics of asyncHandler

Writing try/catch blocks inside every route is repetitive and prone to silent failures. express-response-engine exports asyncHandler as its default export, which acts as a wrapper for Express controllers.

How it works internally:

function asyncHandler(fn) {
  return (req, res, next) => {
    Promise.resolve(fn(req, res, next)).catch(next);
  };
}
  • Auto-Catching: Any thrown error or rejected promise inside an async router callback is caught immediately and forwarded to Express's next(err) pipeline.
  • Zero Boilerplate: By wrapping your controllers in asyncHandler(), you can throw errors (ApiError or CustomError) inline without writing any try/catch blocks. The global errorInterceptor will catch them automatically.

Payload Encryption (AES-256-GCM)

express-response-engine has built-in response payload encryption using Node's native crypto library.

1. Encrypting only the data payload

By default, setting encrypt will only cipher the content of the data (or configured dataKey) property, keeping the other structural metadata keys readable:

app.use(responseInterceptor({
  encrypt: { secretKey: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef' }
}));

2. Encrypting the entire response body

If your security policy requires encrypting the entire HTTP response (including metadata like success status, correlation ID, etc.), set encryptEntireResponse: true:

app.use(responseInterceptor({
  encryptEntireResponse: true,
  encrypt: { secretKey: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef' }
}));
// Returns a single encrypted string of format "iv:auth_tag:ciphertext" to the client

3. Custom Encryption Callbacks

You can supply a custom encryption callback to integrate with custom public key algorithms or external libraries:

app.use(responseInterceptor({
  encryptEntireResponse: true,
  encrypt: (serializedPayload) => {
    return customCipher(serializedPayload);
  }
}));

Logging Hook Setup

You can plug in your own Winston, Pino, or custom logger function to capture request durations, status codes, and error trace lines:

const winston = require('winston');
const logger = winston.createLogger({ /* ... */ });

// Hook into Success responses:
app.use(responseInterceptor({
  logger: (message, meta) => {
    logger.info(message, meta);
  }
}));

// Hook into Error handlers:
app.use(errorInterceptor({
  logger: (message, meta) => {
    logger.error(message, meta);
  }
}));

Log Metadata payload:

The meta payload supplied to loggers includes:

  • method (HTTP verb)
  • url (original route path)
  • statusCode
  • durationMs (execution time)
  • requestId (correlation ID)
  • error and stack trace (for error handlers)

CLI: Server Generator

express-response-engine ships with a built-in CLI that auto-generates a ready-to-run Express server file so you can skip writing all the boilerplate yourself.

Quick Start

# Using npx (no install required)
npx express-response-engine init

# Or via npm script (after npm install)
npm run create-server

Smart Output Path

The CLI automatically detects where to place the generated file:

| Your project has | Generated file | |----------------------|------------------------------| | A src/ directory | src/index.js | | No src/ directory | index.js (root) | | --ts flag | src/index.ts or index.ts |

Flags & Options

# Generate a TypeScript starter file
npx express-response-engine init --ts

# Specify a custom output path
npx express-response-engine init server/app.js

# Overwrite an existing file
npx express-response-engine init --force

# Show help
npx express-response-engine --help

What Gets Generated

The generated index.js (or index.ts) is a fully wired Express server:

'use strict';

const express = require('express');
const {
  responseInterceptor,
  errorInterceptor,
  asyncHandler,
  ApiError,
} = require('express-response-engine');

const app = express();
const PORT = process.env.PORT || 3000;

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

// Health Check Route
app.get('/health', (req, res) => {
  res.check(
    { status: 'ok', uptime: process.uptime(), timestamp: new Date().toISOString() },
    'Server is healthy'
  );
});

// Example Route
app.get(
  '/api/example',
  asyncHandler(async (req, res) => {
    return res.success({ message: 'Hello from express-response-engine!' });
  })
);

// Global Error Handler -- must be LAST
app.use(errorInterceptor());

app.listen(PORT, () => {
  console.log(`Server is running at http://localhost:${PORT}`);
  console.log(`Health check: http://localhost:${PORT}/health`);
});

Running the Generated Server

# Install dependencies first (if not already)
npm install express express-response-engine

# Start the server
node index.js
# or
node src/index.js

# Override port via environment variable
PORT=8080 node index.js

Health Check Route

Once running, visit:

GET http://localhost:3000/health

Returns:

{
  "success": true,
  "statusCode": 200,
  "message": "Server is healthy",
  "data": {
    "status": "ok",
    "uptime": 3.14,
    "timestamp": "2026-08-12T10:00:00.000Z"
  }
}

CLI: Module Generator

Scaffold a complete Router → Controller → Service → Validation module for any resource in one command.

npx express-response-engine module <name>

What Gets Generated

Running module user creates 4 files:

src/modules/user/
├── user.router.js       <- Express Router: wires routes + validation + controller
├── user.controller.js   <- asyncHandler handlers, calls service, uses res.*()
├── user.service.js      <- Business logic, throws ApiError for domain errors
└── user.validation.js   <- express-validator rules + validate() helper

Requires: npm install express-validator

Flags

# JavaScript (default)
npx express-response-engine module user

# TypeScript
npx express-response-engine module user --ts

# Custom output directory
npx express-response-engine module product --dir api/modules

# Overwrite existing files
npx express-response-engine module user --force

# Alias
npx express-response-engine create-module user

Smart Output Path

| Your project has | Generated path | |----------------------|---------------------------| | A src/ directory | src/modules/<name>/ | | No src/ directory | modules/<name>/ | | --dir custom/path | custom/path/<name>/ |

Generated File Overview

user.validation.jsexpress-validator rules with a validate() helper that automatically formats errors and calls next(ApiError.validationError(...)):

const { createRules, getByIdRules, updateRules } = require('./user.validation');

user.service.js — Pure business logic with in-memory placeholder (swap for Mongoose / Prisma / TypeORM):

// Throws ApiError.notFound(), ApiError.conflict() automatically
const user = await userService.getById(id);

user.controller.js — Each method wrapped with asyncHandler, calls service, returns standard response:

const getById = asyncHandler(async (req, res) => {
  const data = await userService.getById(req.params.id);
  return res.success(data, 'User retrieved successfully');
});

user.router.js — Wires all routes with validation middleware:

router.post('/', createRules, controller.create);     // validation runs first
router.get('/:id', getByIdRules, controller.getById);
router.put('/:id', updateRules, controller.update);
router.delete('/:id', getByIdRules, controller.remove);

Mounting the Router

After generating, add one line to your server file:

// index.js
const userRouter = require('./src/modules/user/user.router');
app.use('/api/users', userRouter);
// index.ts
import userRouter from './src/modules/user/user.router';
app.use('/api/users', userRouter);

Available Endpoints

After mounting at /api/users:

| Method | Path | Description | |----------|------------------|--------------------------| | GET | /api/users | List all users | | GET | /api/users/:id | Get user by ID | | POST | /api/users | Create user (validated) | | PUT | /api/users/:id | Update user (validated) | | DELETE | /api/users/:id | Delete user |

Validation Error Response (automatic)

POST /api/users with missing/invalid fields returns:

{
  "success": false,
  "statusCode": 422,
  "message": "Validation Error",
  "errors": [
    { "field": "email", "message": "Invalid email format", "value": "bad-email" },
    { "field": "password", "message": "Password must be at least 8 characters" }
  ]
}

License

ISC