Files
skills/docs/api-stack/api/api-plugin-cms/src/routes/problem-json.md
2026-05-29 08:20:56 +01:00

3.2 KiB

RFC 7807 problem+json renderer

(no submodules)

Purpose

Render CmsError (and arbitrary throwables) as RFC 7807 problem+json HTTP responses without requiring @suaveplan/api-plugin-cms to depend on any HTTP framework. The renderer accepts a small structural HttpResponseLike interface that is compatible with Hono, Fastify, Bun's stdlib Response, and any framework whose response object exposes header(name, value) plus body(data, status). Status mapping is encoded inline as a Record<string, number> lookup table that the package-level spec-conformance test asserts is exhaustive over CMS_CODES.

Features

  • Pure formatProblemJson(err) returns the JSON body for logging or side-channel use.
  • respondProblemJson(res, err) writes header + body and returns the framework's Response for return res.body(...) style handlers.
  • PROBLEM_JSON_CONTENT_TYPE constant — application/problem+json per RFC 7807.
  • Local ProblemDetails interface matching the IETF spec, with an extension-index signature so callers can add fields per RFC §3.2.
  • Status table:
    • CMS.CONTENT_NOT_FOUND / CMS.MODEL_NOT_FOUND → 404
    • CMS.INVALID_TRANSITION / CMS.DUPLICATE_SLUG / CMS.MODEL_IN_USE → 409
    • CMS.VALIDATION_FAILED → 400
    • CMS.INVALID_INPUT → 500
  • Generic SuaveplanError instances render with HTTP 500 but with the original code so log scrapers still see structure.
  • Non-error throwables render { type: "about:blank", status: 500 }.

Usage: Basic

import { Hono } from "hono";
import { respondProblemJson } from "@suaveplan/api-plugin-cms";

const app = new Hono();
app.onError((err, c) => respondProblemJson(c, err));

Usage: Advanced

Side-channel logging without writing to a response (e.g. forwarding the body to an audit pipeline):

import { formatProblemJson } from "@suaveplan/api-plugin-cms";

try {
    engine.transitionStatus(id, target);
} catch (err) {
    const body = formatProblemJson(err);
    await audit.log({
        type: "cms.transition.failed",
        severity: body.status >= 500 ? "error" : "warning",
        ...body,
    });
    throw err;
}

API Reference

Symbol Description
PROBLEM_JSON_CONTENT_TYPE "application/problem+json".
interface ProblemDetails RFC 7807 body shape with an extension index.
interface HttpResponseLike { header(n, v); body(d, s): Response }.
respondProblemJson(res, err) Write header + body; return Response.
formatProblemJson(err) Pure variant.

Implementation Notes

The status lookup is a Readonly<Record<string, number>> keyed by the fully-qualified CMS.* code. If a CmsError is constructed with a code not in the table (unreachable in normal use; possible only in tests that bypass defineCodes), the renderer falls back to HTTP 500. Generic SuaveplanError instances render with HTTP 500 + the original code field so the response is still grep-friendly. The HttpResponseLike shape was chosen over Context (Hono) to keep this package free of framework dependencies — see @suaveplan/api-plugin-database's routes/problem-json.ts for the same pattern.