Response Formatter
Generate consistent API responses using a framework-agnostic formatter with lightweight adapters for Express, Fastify, and Hono.
The Response Formatter block provides a consistent response format for your APIs.
It separates response formatting from your web framework by exposing a framework-agnostic core and lightweight adapters. The core is responsible only for building response objects, while adapters send those responses using the APIs of your chosen framework.
Features
- Framework-agnostic response formatter
- Consistent success and error response structure
- Built-in pagination metadata calculation
- Lightweight adapters for Express, Fastify, and Hono
- Type-safe response contracts
- Optional request ID support
File Structure
response-formatter
├── adapters
│ ├── express.ts
│ ├── fastify.ts
│ └── hono.ts
├── core.ts
└── contract.ts- adapters/ — Framework-specific classes (
ExpressResponse,FastifyResponse,HonoResponse) that wrap the core formatter. - core.ts —
ResponseFormatterclass withsuccess(),paginated(), anderror()static methods. - contract.ts — TypeScript interfaces for response shapes (
SuccessResponse,ErrorResponse,ApiResponse,PaginationMeta).
Installation
pnpm dlx blockend-cli add response-formatterDetect Project
Blockend detects your backend framework and installs the appropriate adapter automatically.
Generate Files
The formatter block is generated inside your configured blocks directory.
Copy the files below into your project's blocks directory.
Peer Dependencies
| Package | Required for |
|---|---|
express | Express adapter |
fastify | Fastify adapter |
hono | Hono adapter |
blocks/response-formatter/contract.ts
TypeScript interfaces for API response shapes.
export interface PaginationParams {
page: number;
limit: number;
total: number;
}
export interface PaginationMeta extends PaginationParams {
totalPages: number;
hasNext: boolean;
hasPrevious: boolean;
}
export interface SuccessResponse<T> {
success: true;
data: T;
message: string;
requestId?: string;
meta?: PaginationMeta;
}
export interface ErrorResponse {
success: false;
data: null;
error: {
message: string;
details?: unknown;
};
requestId?: string;
}
export type ApiResponse<T> = SuccessResponse<T> | ErrorResponse;
blocks/response-formatter/core.ts
ResponseFormatter class with success, paginated, and error static methods.
import type { ApiResponse, ErrorResponse, PaginationParams } from "./contract";
export class ResponseFormatter {
static success<T>(data: T, message: string, requestId?: string): ApiResponse<T> {
return {
success: true,
data,
message,
...(requestId ? { requestId } : {})
};
}
static paginated<T>(
data: T,
message: string,
pagination: PaginationParams,
requestId?: string
): ApiResponse<T> {
const page = Math.max(1, pagination.page);
const limit = Math.max(1, pagination.limit);
const total = Math.max(0, pagination.total);
const totalPages = Math.ceil(total / limit);
return {
success: true,
data,
message,
meta: {
page,
limit,
total,
totalPages,
hasNext: page < totalPages,
hasPrevious: page > 1 && totalPages > 0
},
...(requestId ? { requestId } : {})
};
}
static error(error: { message: string; details?: unknown }, requestId?: string): ErrorResponse {
return {
success: false,
data: null,
error,
...(requestId ? { requestId } : {})
};
}
}
blocks/response-formatter/adapters/express.ts
Express adapter — ExpressResponse class.
import type { Response } from "express";
import type { PaginationParams } from "../contract";
import { ResponseFormatter } from "../core";
export class ExpressResponse {
static success<T>(
res: Response,
data: T,
message = "Request completed successfully",
statusCode = 200,
requestId?: string
) {
return res.status(statusCode).json(ResponseFormatter.success(data, message, requestId));
}
static paginated<T>(
res: Response,
data: T,
message: string,
pagination: PaginationParams,
statusCode = 200,
requestId?: string
) {
return res
.status(statusCode)
.json(ResponseFormatter.paginated(data, message, pagination, requestId));
}
static error(
res: Response,
statusCode: number,
message: string,
details?: unknown,
requestId?: string
) {
return res.status(statusCode).json(
ResponseFormatter.error(
{
message,
...(details ? { details } : {})
},
requestId
)
);
}
}
blocks/response-formatter/adapters/fastify.ts
Fastify adapter — FastifyResponse class.
import type { FastifyReply } from "fastify";
import type { PaginationParams } from "../contract";
import { ResponseFormatter } from "../core";
export class FastifyResponse {
static success<T>(
reply: FastifyReply,
data: T,
message = "Request completed successfully",
statusCode = 200,
requestId?: string
) {
return reply.status(statusCode).send(ResponseFormatter.success(data, message, requestId));
}
static paginated<T>(
reply: FastifyReply,
data: T,
message: string,
pagination: PaginationParams,
statusCode = 200,
requestId?: string
) {
return reply
.status(statusCode)
.send(ResponseFormatter.paginated(data, message, pagination, requestId));
}
static error(
reply: FastifyReply,
statusCode: number,
message: string,
details?: unknown,
requestId?: string
) {
return reply.status(statusCode).send(
ResponseFormatter.error(
{
message,
...(details ? { details } : {})
},
requestId
)
);
}
}
blocks/response-formatter/adapters/hono.ts
Hono adapter — HonoResponse class.
import type { Context } from "hono";
import type { ContentfulStatusCode } from "hono/utils/http-status";
import type { PaginationParams } from "../contract";
import { ResponseFormatter } from "../core";
export class HonoResponse {
static success<T>(
c: Context,
data: T,
message = "Request completed successfully",
statusCode: ContentfulStatusCode = 200,
requestId?: string
) {
return c.json(ResponseFormatter.success(data, message, requestId), statusCode);
}
static paginated<T>(
c: Context,
data: T,
message: string,
pagination: PaginationParams,
statusCode: ContentfulStatusCode = 200,
requestId?: string
) {
return c.json(ResponseFormatter.paginated(data, message, pagination, requestId), statusCode);
}
static error(
c: Context,
statusCode: ContentfulStatusCode,
message: string,
details?: unknown,
requestId?: string
) {
return c.json(
ResponseFormatter.error(
{
message,
...(details ? { details } : {})
},
requestId
),
statusCode
);
}
}
Configuration
| Export | Description |
|---|---|
ResponseFormatter | Core formatter class with success(), paginated(), error() |
ExpressResponse | Express adapter class |
FastifyResponse | Fastify adapter class |
HonoResponse | Hono adapter class |
SuccessResponse | Type for successful responses |
ErrorResponse | Type for error responses |
ApiResponse | Union type of SuccessResponse and ErrorResponse |
PaginationMeta | Type for pagination metadata |
PaginationParams | Type for pagination input (page, limit, total) |
Architecture
Route Handler
│
▼
Adapter (ExpressResponse / FastifyResponse / HonoResponse)
│ (delegates to core formatter)
▼
ResponseFormatter (Core)
│ (builds plain response object)
▼
Adapter sends response via framework API
│ (res.json / reply.send / c.json)
▼
HTTP ResponseThe adapter never contains formatting logic — it delegates entirely to the core formatter. The core formatter never imports any framework — it returns plain JavaScript objects. This separation keeps the formatting logic reusable across frameworks.
When to Use
- You want consistent JSON response shapes across your entire API.
- You need built-in pagination metadata calculation.
- You use multiple frameworks and want a single formatting approach.
When Not to Use
- You prefer to construct responses manually in each route handler.
- You don't need pagination or standardized error shapes.
- You use a framework with built-in response formatting (like NestJS).
Usage
Express
import { ExpressResponse } from "@/blocks/response-formatter/adapters/express";
app.get("/users/:id", (req, res) => {
const user = { id: 1, name: "Alice" };
return ExpressResponse.success(res, user, "User fetched successfully");
});Fastify
import { FastifyResponse } from "@/blocks/response-formatter/adapters/fastify";
fastify.get("/users/:id", async (request, reply) => {
const user = { id: 1, name: "Alice" };
return FastifyResponse.success(reply, user, "User fetched successfully");
});Hono
import { HonoResponse } from "@/blocks/response-formatter/adapters/hono";
app.get("/users/:id", (c) => {
const user = { id: 1, name: "Alice" };
return HonoResponse.success(c, user, "User fetched successfully");
});Error Responses
return ExpressResponse.error(res, 404, "User not found");
// With error details
return ExpressResponse.error(res, 400, "Validation failed", {
email: ["Invalid email address"]
});Paginated Responses
return ExpressResponse.paginated(res, users, "Users fetched successfully", {
page: 2,
limit: 10,
total: 53
});Response:
{
"success": true,
"data": [...],
"message": "Users fetched successfully",
"meta": {
"page": 2,
"limit": 10,
"total": 53,
"totalPages": 6,
"hasNext": true,
"hasPrevious": true
}
}Request IDs
Include a request ID in the response for debugging:
ExpressResponse.success(res, user, "Success", 200, req.id);API Reference
ResponseFormatter.success
static success<T>(data: T, message: string, requestId?: string): ApiResponse<T>;Creates a successful response object.
ResponseFormatter.paginated
static paginated<T>(
data: T,
message: string,
pagination: PaginationParams,
requestId?: string
): ApiResponse<T>;Creates a paginated response. Automatically computes totalPages, hasNext, and hasPrevious.
ResponseFormatter.error
static error(error: { message: string; details?: unknown }, requestId?: string): ErrorResponse;Creates an error response object.
ExpressResponse
static success<T>(res: Response, data: T, message?: string, statusCode?: number, requestId?: string): void;
static paginated<T>(res: Response, data: T, message: string, pagination: PaginationParams, statusCode?: number, requestId?: string): void;
static error(res: Response, statusCode: number, message: string, details?: unknown, requestId?: string): void;FastifyResponse / HonoResponse
Same interface as ExpressResponse but accepts FastifyReply or HonoContext as the first argument.
PaginationParams
interface PaginationParams {
page: number;
limit: number;
total: number;
}SuccessResponse
interface SuccessResponse<T> {
success: true;
data: T;
message: string;
requestId?: string;
meta?: PaginationMeta;
}ErrorResponse
interface ErrorResponse {
success: false;
data: null;
error: {
message: string;
details?: unknown;
};
requestId?: string;
}Examples
Using the Core Formatter Directly
Use the core formatter without adapters — it returns plain objects you can send however you want:
import { ResponseFormatter } from "@/blocks/response-formatter/core";
const response = ResponseFormatter.success(user, "User fetched successfully");
res.json(response);Related Blocks
- Error Handler — Combine with
ExpressResponse.error()for consistent error shapes across your API.
FAQ
Why does the formatter only need page, limit, and total for pagination?
totalPages, hasNext, and hasPrevious are deterministic — they can always be calculated from page, limit, and total. Keeping this computation inside the formatter prevents bugs and ensures consistent pagination behavior across your entire application.
Can I use the core formatter without adapters?
Yes. ResponseFormatter.success(), .paginated(), and .error() return plain JavaScript objects. You can use them directly and send the response however you like.
How do I add custom fields to the response?
Use the extension point by calling the core formatter and merging your custom fields before sending the response.
Request Validator
Validate incoming HTTP requests using Zod with a framework-agnostic core and lightweight adapters for Express, Fastify, and Hono.
Environment Configuration
Type-safe environment variable validation with Zod. Fail fast at startup and access fully typed configuration throughout your application.