RFC 9457 Problem Details middleware for Hono.
Returns application/problem+json structured error responses with a single app.onError setup.
- RFC 9457 compliant — standard 5 fields + extension members
- Hono native —
app.onErrorhandler +createMiddleware()patterns - Zod integration —
@hono/zod-validatorhook for validation errors - Valibot integration —
@hono/valibot-validatorhook for validation errors - OpenAPI integration —
@hono/zod-openapischemas for API documentation - Standard Schema —
@hono/standard-validatorhook (works with any schema library) - Type-safe — full TypeScript support with inference
- Zero external dependencies — only
honoas peer dependency - Localization —
localizecallback for title/detail translation - Edge-first — works on Cloudflare Workers, Deno, Bun, and Node.js
npm install hono-problem-detailsimport { Hono } from "hono";
import { problemDetailsHandler } from "hono-problem-details";
const app = new Hono();
app.onError(problemDetailsHandler());
app.get("/not-found", (c) => {
throw new HTTPException(404, { message: "Resource not found" });
});
// Response:
// HTTP/1.1 404 Not Found
// Content-Type: application/problem+json
// {
// "type": "about:blank",
// "status": 404,
// "title": "Not Found",
// "detail": "Resource not found"
// }import { problemDetails } from "hono-problem-details";
app.post("/orders", (c) => {
throw problemDetails({
status: 409,
title: "Conflict",
detail: `Order ${id} already exists`,
type: "https://api.example.com/problems/order-conflict",
instance: `/orders/${id}`,
});
});Extension members are flattened to top level per RFC 9457:
throw problemDetails({
status: 422,
title: "Validation Error",
extensions: {
errors: [
{ field: "email", message: "must be a valid email" },
],
},
});
// Response body:
// {
// "type": "about:blank",
// "status": 422,
// "title": "Validation Error",
// "errors": [{ "field": "email", "message": "must be a valid email" }]
// }Pre-define your API's error types for type-safe error creation:
import { createProblemTypeRegistry } from "hono-problem-details";
const problems = createProblemTypeRegistry({
ORDER_CONFLICT: {
type: "https://api.example.com/problems/order-conflict",
status: 409,
title: "Order Conflict",
},
RATE_LIMITED: {
type: "https://api.example.com/problems/rate-limited",
status: 429,
title: "Too Many Requests",
},
});
// Type-safe error creation
app.post("/orders", (c) => {
throw problems.create("ORDER_CONFLICT", {
detail: `Order ${id} already exists`,
instance: `/orders/${id}`,
});
});
// With extensions
throw problems.create("RATE_LIMITED", {
extensions: { retryAfter: 60 },
});import { zValidator } from "@hono/zod-validator";
import { zodProblemHook } from "hono-problem-details/zod";
import { z } from "zod";
const schema = z.object({
email: z.string().email(),
age: z.number().positive(),
});
app.post("/users", zValidator("json", schema, zodProblemHook()), (c) => {
const data = c.req.valid("json");
// ...
});
// Validation error response:
// HTTP/1.1 422 Unprocessable Content
// Content-Type: application/problem+json
// {
// "type": "about:blank",
// "status": 422,
// "title": "Validation Error",
// "detail": "Request validation failed",
// "errors": [{ "field": "email", "message": "Invalid email", "code": "invalid_string" }]
// }import { vValidator } from "@hono/valibot-validator";
import { valibotProblemHook } from "hono-problem-details/valibot";
import * as v from "valibot";
const schema = v.object({
email: v.pipe(v.string(), v.email()),
age: v.pipe(v.number(), v.minValue(1)),
});
app.post("/users", vValidator("json", schema, valibotProblemHook()), (c) => {
const data = c.req.valid("json");
// ...
});Works with any Standard Schema compatible library (Zod, Valibot, ArkType, etc.):
import { sValidator } from "@hono/standard-validator";
import { standardSchemaProblemHook } from "hono-problem-details/standard-schema";
import { z } from "zod"; // or valibot, arktype, etc.
const schema = z.object({
email: z.string().email(),
});
app.post("/users", sValidator("json", schema, standardSchemaProblemHook()), (c) => {
const data = c.req.valid("json");
// ...
});Use with @hono/zod-openapi to document Problem Details error responses in your OpenAPI spec:
import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi";
import { problemDetailsHandler } from "hono-problem-details";
import {
ProblemDetailsSchema,
createProblemDetailsSchema,
problemDetailsResponse,
} from "hono-problem-details/openapi";
const app = new OpenAPIHono();
app.onError(problemDetailsHandler());
// Use problemDetailsResponse() in route definitions
const route = createRoute({
method: "get",
path: "/users/{id}",
request: {
params: z.object({ id: z.string() }),
},
responses: {
200: {
content: {
"application/json": {
schema: z.object({ id: z.string(), name: z.string() }),
},
},
description: "User found",
},
404: problemDetailsResponse(404),
422: problemDetailsResponse(422, "Validation Error"),
},
});
// With extension members
const errorWithExtensions = createProblemDetailsSchema(
z.object({
errors: z.array(z.object({ field: z.string(), message: z.string() })),
}),
);
// Use: problemDetailsResponse(422, "Validation Error", errorWithExtensions)Use the localize callback to translate title and detail based on the request context:
problemDetailsHandler({
localize: (pd, c) => {
const lang = c.req.header("Accept-Language");
if (lang?.startsWith("ja")) {
return { ...pd, title: translate("ja", pd.title) };
}
return pd;
},
});The callback receives the fully-built ProblemDetails object and the Hono Context, allowing access to headers like Accept-Language. Return a new ProblemDetails with translated fields.
problemDetailsHandler({
// Prefix for type URI (e.g., "https://api.example.com/problems")
typePrefix: "https://api.example.com/problems",
// Default type URI (default: "about:blank")
defaultType: "about:blank",
// Include stack trace in detail (for development)
includeStack: process.env.NODE_ENV === "development",
// Localize title/detail before sending the response
localize: (pd, c) => ({ ...pd, title: `[${lang}] ${pd.title}` }),
// Custom error mapping
mapError: (error) => {
if (error instanceof MyCustomError) {
return {
status: error.statusCode,
title: error.name,
detail: error.message,
};
}
return undefined; // fallback to default handling
},
});The following Hono middleware libraries use hono-problem-details as an optional dependency for RFC 9457 error responses:
- hono-idempotency — Idempotency key middleware for Hono
- hono-webhook-verify — Webhook signature verification middleware for Hono
MIT