@0xzahed/api-response-toolkit
v1.0.3
Published
Standardize API responses, errors, and pagination for Express and Fastify apps.
Maintainers
Readme
@0xzahed/api-response-toolkit
Standardize API responses, error handling, and pagination across your Express or Fastify backend — so every endpoint returns the same predictable JSON shape.
{
"success": true,
"message": "User fetched",
"data": { "id": 1, "name": "Ada" },
"meta": null,
"timestamp": "2026-09-06T18:00:00.000Z"
}Why
Without a convention, every route in a codebase ends up shaping its JSON differently — {data}, {result}, {user}, raw arrays, inconsistent error fields. This makes frontend consumption and error handling unpredictable. This toolkit gives you one shape for success, one shape for errors, and one shape for paginated lists, plus the middleware to enforce it with almost no boilerplate.
Install
npm install @0xzahed/api-response-toolkitExpress and Fastify are peer dependencies — install whichever framework you use:
npm install express
# or
npm install fastifyQuick start — Express
import express from "express";
import {
responseToolkit,
errorHandler,
asyncHandler,
NotFoundError,
} from "@0xzahed/api-response-toolkit/express";
const app = express();
app.use(express.json());
app.use(responseToolkit()); // attaches res.success / res.error / res.paginate
app.get("/users/:id", asyncHandler(async (req, res) => {
const user = await db.users.find(req.params.id);
if (!user) throw new NotFoundError("User not found");
res.success(user, "User fetched");
}));
app.get("/users", asyncHandler(async (req, res) => {
const { rows, total } = await db.users.list({ page: 1, limit: 20 });
res.paginate(rows, { page: 1, limit: 20, total });
}));
app.use(errorHandler()); // register LAST — formats thrown errors
app.listen(3000);Quick start — Fastify
import Fastify from "fastify";
import { responseToolkit, NotFoundError } from "@0xzahed/api-response-toolkit/fastify";
const fastify = Fastify();
await fastify.register(responseToolkit);
fastify.get("/users/:id", async (req, reply) => {
const user = await db.users.find(req.params.id);
if (!user) throw new NotFoundError("User not found");
reply.success(user, "User fetched");
});
fastify.get("/users", async (req, reply) => {
const { rows, total } = await db.users.list({ page: 1, limit: 20 });
reply.paginate(rows, { page: 1, limit: 20, total });
});
fastify.listen({ port: 3000 });Fastify's global error handler (registered automatically by the plugin) formats any thrown AppError the same way as Express.
Response shapes
Success — success(data, message?, meta?)
{ "success": true, "message": "Success", "data": {}, "meta": null, "timestamp": "..." }Error — error(message?, statusCode?, errorCode?, details?)
{ "success": false, "message": "Not found", "errorCode": "NOT_FOUND", "details": null, "timestamp": "..." }Paginated — paginate(data, { page, limit, total })
{
"success": true,
"data": [],
"pagination": {
"page": 1, "limit": 20, "total": 87,
"totalPages": 5, "hasNext": true, "hasPrev": false
}
}Error classes
All extend AppError and carry a matching statusCode and errorCode, so throwing them from any route (sync or async, wrapped in asyncHandler) results in the correctly formatted error response automatically.
| Class | Status | Code |
|---|---|---|
| NotFoundError | 404 | NOT_FOUND |
| ValidationError | 400 | VALIDATION_ERROR |
| UnauthorizedError | 401 | UNAUTHORIZED |
| ForbiddenError | 403 | FORBIDDEN |
| ConflictError | 409 | CONFLICT |
| AppError | custom | custom |
throw new ValidationError("Invalid email", { field: "email" });
throw new AppError("Rate limited", 429, "RATE_LIMITED");Unexpected errors (anything that isn't an AppError, e.g. a database connection failure) are caught by the global error handler and returned as a generic 500 / INTERNAL_ERROR with message "Internal server error" — the original error message is never leaked to the client. Client errors (4xx) from framework-level failures (e.g. malformed JSON body, schema validation) are surfaced with a REQUEST_ERROR or VALIDATION_ERROR code and their status preserved.
API reference
Core (framework-agnostic)
Import from "@0xzahed/api-response-toolkit":
success(data, message?, meta?)error(message?, statusCode?, errorCode?, details?)paginate(data, { page, limit, total })AppError,NotFoundError,ValidationError,UnauthorizedError,ForbiddenError,ConflictError
Express (@0xzahed/api-response-toolkit/express)
responseToolkit()— middleware attachingres.success,res.error,res.paginateerrorHandler(options?)— global error-formatting middleware (register last);options.loggeroverrides the defaultconsole.errorasyncHandler(fn)— wraps an async route handler so rejected promises reacherrorHandlerwithout try/catch
Fastify (@0xzahed/api-response-toolkit/fastify)
responseToolkit— plugin (register withfastify.register(...)) attachingreply.success,reply.error,reply.paginate, and a global error handler
TypeScript
Fully typed — response shapes, error classes, and framework decorators (res.success, reply.error, etc.) all have proper type definitions, including augmented Request/Reply types for Express and Fastify.
License
MIT
