Skip to content

Validation & Hono

Shared Standard Schema validation errors and drop-in Hono middleware.

@nice-code/common-errors gives you a ready-made error domain for Standard Schema validation failures, plus Hono middleware that throws it for you.

Terminal window
bun add @nice-code/common-errors

Peer deps: valibot (or any Standard Schema library), hono (for the /hono subpath).

err_validation is a @nice-code/error domain. Import it to match or inspect validation errors by domain. It exposes one id — EValidator.standard_schema — with context { issues }.

import { err_validation, EValidator } from "@nice-code/common-errors";
if (err_validation.isExact(caught)) {
const hydrated = err_validation.hydrate(caught);
const { issues } = hydrated.getContext(EValidator.standard_schema);
// issues: readonly StandardSchemaV1.Issue[]
}

Import from the /hono subpath.

A drop-in replacement for @hono/standard-validator’s sValidator. When validation fails it throws a NiceError instead of returning a 400 — so all your error handling goes through one place.

import { niceSValidator } from "@nice-code/common-errors/hono";
import * as v from "valibot";
const CreateUserSchema = v.object({
name: v.string(),
email: v.pipe(v.string(), v.email()),
});
app.post("/users", niceSValidator("json", CreateUserSchema), async (c) => {
const body = c.req.valid("json"); // fully typed
// ...
});

Convert the thrown error to a response in Hono’s onError:

import { castNiceError } from "@nice-code/error";
import type { ContentfulStatusCode } from "hono/utils/http-status";
app.onError((err, c) => {
const niceError = castNiceError(err);
// `httpStatusCode` is a plain `number`; Hono's `c.json` only accepts its restricted
// status-code union, so the cast bridges the two. The value is a real HTTP code at runtime.
return c.json(niceError.toJsonObject(), niceError.httpStatusCode as ContentfulStatusCode);
});

If something that isn’t a NiceError crashes the request — a dropped DB connection, a thrown string, a bug deep in a dependency — you don’t need a separate 500 branch. castNiceError wraps any non-NiceError value in a generic NiceError flagged isUnhandled: true, with a default httpStatusCode of 500. So the single onError above already returns a well-formed JSON error with a sensible status for every failure path; the isUnhandled flag is what lets you tell a declared, expected error apart from an unexpected crash (e.g. to log it differently or hide its message in production).

Catches the raw responses @hono/standard-validator sends back (its default { success: false, error: [...] } shape) and turns them into NiceError JSON responses — useful when you can’t swap sValidator out yourself.

import { niceCatchSValidation } from "@nice-code/common-errors/hono";
app.use(niceCatchSValidation());
// Existing sValidator usage is now intercepted automatically
app.post("/data", sValidator("json", MySchema), handler);
import { Hono } from "hono";
import type { ContentfulStatusCode } from "hono/utils/http-status";
import { niceSValidator } from "@nice-code/common-errors/hono";
import { castNiceError } from "@nice-code/error";
import * as v from "valibot";
const app = new Hono();
app.onError((err, c) => {
const niceError = castNiceError(err);
return c.json(niceError.toJsonObject(), niceError.httpStatusCode as ContentfulStatusCode);
});
const BodySchema = v.object({ message: v.string() });
app.post("/echo", niceSValidator("json", BodySchema), async (c) => {
const { message } = c.req.valid("json");
return c.json({ echo: message });
});