Production-ready Express API response standardizer. Features centralized error handling, Mongoose error normalization, smart pagination, and DTO transforms.
npm install express-unified-responseA production-ready response, error handling, and pagination toolkit for Express + Mongoose APIs.
It enforces consistent API responses, centralized error handling, and powerful pagination with minimal boilerplate.
- Standard success, created, updated, deleted responses
- Configurable response keys (success, data, meta, message, error)
- Built-in REST semantics (204 No Content, 201 Created, etc.)
- Custom AppError hierarchy
- Automatic conversion of Mongoose errors (Validation, Cast, Duplicate Key).
- Handles JWT, File Upload, and Axios errors automatically.
- Safe defaults for production (no stack leaks)
- Paginate standard Mongoose queries and aggregation pipelines.
- Supports DTO / Transform functions to clean data.
- Enforces maxLimit automatically to protect your database.
Methods are attached directly to the res object: res.success(), res.created(), res.updated(), res.deleted(), res.list(), res.paginateQuery(), res.paginateAggregate().
- Rename response keys
- Customize pagination labels
- Control REST defaults
- Plug in custom logging
- Enable/disable stack traces
``bash`
npm install express-unified-response
> Requirements
- Node.js ≥ 20
- Express
- Mongoose
`js
import express from "express";
import {
createResponseMiddleware,
createErrorMiddleware,
} from "express-unified-response";
const app = express();
app.use(express.json());
// Attach response helpers
app.use(createResponseMiddleware());
// Your routes go here...
// Central error handler (must be last)
app.use(createErrorMiddleware());
export default app;
`
The library follows two intuitive argument patterns:
Standard: (Data, Message, Options)
Paginated: (Data/Model, Options, Message)
`js
// Simple usage
res.success(user, "User fetched successfully");
// With Transform (DTO) and Silent Logging
res.success(user, "OK", {
transform: (u) => ({ id: u._id, name: u.name }),
silent: true,
});
res.created(newUser, "User created");
`
The toolkit handles REST semantics automatically. If data is provided, it returns 200. If data is null/empty, it returns 204.
`js
// Returns 200 + Body
res.updated(updatedUser, "User updated");
// Returns 204 No Content (No body)
res.updated(null);
// Message-only: Returns 200 + Message (Skips data when 1st arg is null/undefined/_)
res.updated(null, "Password changed successfully");
res.deleted(null, "User deleted");
`
Standardizes array responses. Non-paginated lists still receive a meta block for frontend consistency.
`js`
res.list(
users,
{
paginate: true,
page: 1,
limit: 10,
transform: (u) => ({ id: u._id, name: u.name }),
},
"Users fetched successfully"
);
`js
await res.paginateQuery(
UserModel,
{
page: 1,
limit: 10,
filter: { isActive: true },
populate: "profile",
transform: (doc) => ({ id: doc._id, email: doc.email }),
},
"Active users fetched"
);
await res.paginateAggregate(
UserModel,
{
pipeline: [{ $match: { score: { $gt: 80 } } }],
transform: (doc) => ({ id: doc._id, score: doc.score }),
},
"High scorers fetched"
);
`
You can throw custom errors anywhere in your logic; the error middleware will catch and format them.
`js
import { NotFoundError } from "express-unified-response";
app.get("/users/:id", async (req, res) => {
const user = await User.findById(req.params.id);
if (!user) throw new NotFoundError("User not found");
res.success(user);
});
`
| Category | Error Class | HTTP Status |
|---|---|---|
| Client (4xx) | BadRequestError | 400 |
ValidationError | 400 | |
NotFoundError | 404 | |
RateLimitError | 429 | |
| Security | UnauthorizedError | 401 |
TokenExpiredError | 401 | |
ForbiddenError | 403 | |
| Server (5xx) | ExternalServiceError | 502 |
DatabaseError | 503 | |
AppError (Base) | 500 |
You don't always have to throw these manually. The middleware automatically detects and converts:
- Mongoose: ValidationError, CastError, and DuplicateKey (11000) are converted to their respective 400 classes.TokenExpiredError
- JWT: and JsonWebTokenError are mapped to 401.FileUploadError
- Multer: File size limits and unexpected fields are mapped to .
The toolkit provides a asyncHandler utility to catch async errors without try-catch blocks.
`js
import { asyncHandler } from "express-unified-response";
app.get(
"/users",
asyncHandler(async (req, res) => {
const users = await User.find();
res.list(users);
})
);
`
`js✔ ${req.method} ${status} (${duration}ms)
const config = {
keys: {
successKey: "success",
dataKey: "data",
metaKey: "meta",
messageKey: "msg",
errorKey: "err",
},
pagination: {
defaults: {
page: 1,
limit: 10,
maxLimit: 50, // safe limit for paginated lists, limit can never exceed this
},
labels: {
nextPage: "next",
prevPage: "prev",
totalDocs: "totalItems",
docs: "items",
totalPages: "totalPages",
limit: "perPage",
page: "currentPage",
hasPrevPage: "hasPrev",
hasNextPage: "hasNext",
},
},
restDefaults: {
deleteReturnsNoContent: true, // true for 204, false for 200
updateReturnsBody: true, // true for 200, false for 204
nonPaginatedMaxItems: 1000, // safe limit for non-paginated lists
},
logger: {
onSuccess: (req, status, duration) =>
console.log(),✖ ${err.code} [${status}]
onError: (req, err, status) => console.error(),${process.env.NODE_ENV}
},
routeNotFound: true,
error: {
exposeStack: !== "production",`
exposeErrorName: false,
defaultErrorMessage: "An unexpected error occurred",
},
};
app.use(createResponseMiddleware(config));
app.use(createErrorMiddleware(config));
> Note: Pass the same config to createErrorMiddleware(config). to make them consistent.
Success List (Paginated)
`json`
{
"success": true,
"data": [{ "id": "123", "score": 90 }],
"meta": {
"currentPage": 1,
"totalItems": 1,
"totalPages": 1,
"hasNextPage": false
}
}
Error
`json``
{
"success": false,
"msg": "Validation failed",
"err": {
"code": "VALIDATION_ERROR",
"details": [{ "field": "email", "message": "Required" }]
}
}
MIT License
