API Reference

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 '@ts-kizuna/core/schemas';

Shape

{
    "type": "about:blank",
    "title": "Not Found",
    "status": 404,
    "detail": "User not found"
}
FieldDescription
typeProblem type URI. about:blank means "no additional semantics beyond the status code."
titleShort human-readable summary matching the HTTP status phrase.
statusHTTP status code.
detailHuman-readable explanation specific to this occurrence.

Usage in contracts

Use ProblemDetailsSchema in your contract's responses to document error status codes:

import { ProblemDetailsSchema } from '@ts-kizuna/core/schemas';
import { k } from './k';

const usersRoutes = k.routes('users', {
    getUser: {
        method: 'GET',
        path: '/users/:id',
        responses: {
            200: UserSchema,
            401: ProblemDetailsSchema,
            403: ProblemDetailsSchema,
            404: ProblemDetailsSchema,
        },
    },
});

Usage in handlers

Handlers only need to provide detail, and kizuna fills in type, title, and status automatically:

const router = server.router({
    users: {
        getUser: ({ 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 response sent to the client includes the full RFC 9457 envelope:

{
    "type": "about:blank",
    "title": "Not Found",
    "status": 404,
    "detail": "User not found"
}

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 you add domain-specific fields with Zod's native .extend(), and there's no builder function:

import { z } from 'zod';
import { ProblemDetailsSchema } from '@ts-kizuna/core/schemas';
import { k } from './k';

const usersRoutes = k.routes('users', {
    createUser: {
        method: 'POST',
        path: '/users',
        body: CreateUserSchema,
        responses: {
            201: UserSchema,
            409: ProblemDetailsSchema.extend({
                conflictingId: z.string(),
            }),
        },
    },
});

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 '@ts-kizuna/core';
import { ProblemDetailsSchema } from '@ts-kizuna/core/schemas';

const ConflictError = Kizuna.model({
    title: 'ConflictError',
    schema: ProblemDetailsSchema.extend({
        conflictingId: z.string(),
    }),
});

// responses: { 409: ConflictError }

The handler supplies detail plus the declared extensions; type, title, and status are still auto-filled:

return error({
    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 also the cleanest way to preserve an existing API's error fields while 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 '@ts-kizuna/core';

if (isProblemDetails(response.body)) {
    console.error(`${response.body.status}: ${response.body.detail}`);
}

Why Problem Details only

Error responses (status >= 400) must be Problem Details. Handlers cannot return an arbitrary error shape, and the type system enforces it. This is deliberate: a single shared error schema is what lets guards, validation, the type guards above, generated clients, and the OpenAPI spec all compose against one shape. Need extra fields? Use extension members. Need a non-JSON error (proxying, an HTML error page)? Use the adapter's raw-response escape hatch.

Migrating to a legacy wire shape

The wire bytes are produced by a single, pluggable formatter. The default emits application/problem+json. Adapters accept a formatError option to reshape the 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 (a version header, Accept, and so on):

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. This is a migration seam, not a way to define custom errors in the contract, and the contract stays Problem Details. Most migrations don't need it at all (use extension members). See Migrating an existing API.

What produces it

Everything in kizuna that returns a non-success response uses this shape:

SourceExample
Guards (deny)deny(403, 'Forbidden')
Route not found404 for unmatched paths
Method not allowed405 with Allow header
Unsupported media type415 for wrong Content-Type
Handler errors500 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.

On this page