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.
Maintainers
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
resobject. - ✅ 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-engineQuick 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 OKres.created(data, message)- 201 Createdres.updated(data, message)- 200 OKres.deleted(data, message)- 200 OK
Error Helpers (Manual response without throwing):
res.badRequest(message, errors)- 400 Bad Requestres.unauthorized(message)- 401 Unauthorizedres.forbidden(message)- 403 Forbiddenres.notFound(message)- 404 Not Foundres.conflict(message)- 409 Conflictres.validationError(errors, message)- 422 Unprocessable Entityres.tooManyRequests(message)- 429 Too Many Requestsres.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
asyncrouter callback is caught immediately and forwarded to Express'snext(err)pipeline. - Zero Boilerplate: By wrapping your controllers in
asyncHandler(), you can throw errors (ApiErrororCustomError) inline without writing anytry/catchblocks. The globalerrorInterceptorwill 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 client3. 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)statusCodedurationMs(execution time)requestId(correlation ID)errorandstacktrace (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-serverSmart 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 --helpWhat 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.jsHealth Check Route
Once running, visit:
GET http://localhost:3000/healthReturns:
{
"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() helperRequires:
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 userSmart 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.js — express-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
