ProblemDetailsSchema
RFC 9457 Problem Details error response schema (ProblemDetailsSchema) used across kizuna.
The standard RFC 9457 Problem Details error response shape used by kizuna. Matches the format produced by deny() in guards and all built-in error responses (404, 405, 415, etc.).
import { ProblemDetailsSchema } from 'kizunajs/schemas';Shape
{
"type": "about:blank",
"title": "Not Found",
"status": 404,
"detail": "User not found"
}| Field | Description |
|---|---|
type | Problem type URI. about:blank means "no additional semantics beyond the status code." |
title | Short human-readable summary matching the HTTP status phrase. |
status | HTTP status code. |
detail | Human-readable explanation specific to this occurrence. |
Usage in routes
Use ProblemDetailsSchema in a route's responses to document error status codes:
import { ProblemDetailsSchema } from 'kizunajs/schemas';
import { k } from './k';
const usersRoutes = k.routes('users', {
getUser: k
.route({
method: 'GET',
path: '/users/:id',
responses: {
200: UserSchema,
404: ProblemDetailsSchema,
},
})
.handler(/* ... */),
});A route whose auth names an identity gets 401 and 403 as Problem Details. See what it drives.
Usage in handlers
Handlers supply detail; type, title, and status are filled in:
getUser: k
.route({
method: 'GET',
path: '/users/:id',
responses: {
200: UserSchema,
404: ProblemDetailsSchema,
},
})
.handler(({ params, throwError }) => {
const user = userStore.get(params.id);
if (!user) {
return throwError({
status: 404,
body: {
detail: 'User not found',
},
});
}
return {
status: 200,
body: user,
};
}),The client receives the full envelope shown under Shape.
Extension members
RFC 9457 lets a problem carry extra fields, called extension members, alongside the standard envelope. ProblemDetailsSchema is a plain Zod object schema, so add them with .extend():
import { z } from 'zod';
import { ProblemDetailsSchema } from 'kizunajs/schemas';
import { k } from '../k';
const users = k.routes('users', {
createUser: k
.route({
method: 'POST',
path: '/users',
body: CreateUserSchema,
responses: {
201: UserSchema,
409: ProblemDetailsSchema.extend({
conflictingId: z.string(),
}),
},
})
.handler(/* ... */),
});To reuse the extended schema across routes, and give it a name in the generated OpenAPI spec, wrap it in Kizuna.model:
import { z } from 'zod';
import { Kizuna } from 'kizunajs';
import { ProblemDetailsSchema } from 'kizunajs/schemas';
const ConflictError = Kizuna.model({
title: 'ConflictError',
schema: ProblemDetailsSchema.extend({
conflictingId: z.string(),
}),
});
// responses: { 409: ConflictError }The handler supplies detail plus the declared extensions:
return throwError({
status: 409,
body: {
detail: 'A user with that email already exists',
conflictingId: existing.id,
},
});{
"type": "about:blank",
"title": "Conflict",
"status": 409,
"detail": "A user with that email already exists",
"conflictingId": "usr_abc123"
}Extension members are how you preserve an existing API's error fields when moving onto kizuna. See Migrating an existing API.
Checking for a Problem Details body
isProblemDetails is a type guard for the shared error shape, the sibling of isValidationError:
import { isProblemDetails } from 'kizunajs';
if (isProblemDetails(response.body)) {
console.error(`${response.body.status}: ${response.body.detail}`);
}The rule
Every response with status >= 400 must be Problem Details, and the type system enforces it. For extra fields, use extension members. For an error that isn't JSON, such as proxying an upstream body or an HTML error page, use the adapter's raw-response escape hatch.
Migrating to a legacy wire shape
Adapters accept a formatError option to reshape the outgoing bytes for clients that can't move to Problem Details yet. It receives the request, so during a transition you serve the legacy shape to old clients and Problem Details to new ones, branching on whatever signal your API has:
api.mount(app, {
formatError: (problem, { request }) => {
// new clients opt in via a signal you control, here a version header
if (request.headers.get('x-api-version') === '2') {
return { contentType: 'application/problem+json', body: problem };
}
// old clients keep the legacy shape
return {
contentType: 'application/json',
body: { ok: false, error: { code: problem.status, message: problem.detail } },
};
},
});The input is always the canonical problem; only the outgoing bytes change, so what you declared stays Problem Details. Reach for it when extension members can't reproduce the old body. See Migrating an existing API.
What produces it
| Source | Example |
|---|---|
Guards (deny) | deny({ status: 403, body: { detail: 'Forbidden' } }) |
| Route not found | 404 for unmatched paths |
| Method not allowed | 405 with Allow header |
| Unsupported media type | 415 for wrong Content-Type |
| Handler errors | 500 for unhandled exceptions |
All built-in error responses use application/problem+json as the content type.
OpenAPI
ProblemDetailsSchema is a named model, so it appears as ProblemDetails in the generated OpenAPI spec under components/schemas, keeping your API documentation clean and consistent.