@aahoughton/oav-fastify
v1.1.1
Published
Fastify adapter for @aahoughton/oav-core. preValidation hook factory plus standalone helpers (httpRequestFromFastify, renderProblemDetails) for callers composing their own hooks.
Maintainers
Readme
oav-fastify
Fastify adapter for oav-core — a preValidation hook factory plus standalone helpers (httpRequestFromFastify, renderProblemDetails) for callers composing their own hooks.
Same shape as the Express siblings (oav-express4, oav-express5) — only the framework-typed argument and Fastify's hook-vs-middleware distinction differ. Fastify is async-native, so thrown errors and rejected promises propagate to Fastify's error handler automatically, with no try/catch wrapper.
Sibling packages: oav-express4, oav-express5. Same export names, option shapes, and defaults; only the framework-typed argument differs.
Install
# JSON specs only
npm install @aahoughton/oav-core @aahoughton/oav-fastify fastify
# YAML specs + CLI (oav transitively provides oav-core)
npm install @aahoughton/oav @aahoughton/oav-fastify fastifyfastify is a peer dep — your app's existing install satisfies it.
YAML specs.
oav-coreis JSON-only by design (zero runtime deps). If your spec is YAML, either installoavinstead — it bundles the YAML readers and the CLI — or installyamlseparately and parse the spec yourself before passing the parsed object tocreateValidator.
Quick start
import Fastify from "fastify";
import { createValidator } from "@aahoughton/oav-core";
import { validateRequests } from "@aahoughton/oav-fastify";
const validator = createValidator(spec);
const app = Fastify();
app.addHook("preValidation", validateRequests(validator));
app.post("/pets", async () => ({ ok: true }));Invalid requests receive a 400 application/problem+json response (status from httpStatusFor, body from toProblemDetails, Allow header on 405). Valid requests reach the route handlers.
Mount point: preValidation
Fastify runs hooks in a fixed order:
onRequest— request parsing not yet donepreParsing— about to parse the bodypreValidation— body parsed; this is where oav runsvalidation— Fastify's per-route schema validationpreHandler— about to call the route handlerhandler
Mount on preValidation so oav sees the parsed body. If you also have per-route Fastify schemas declared, Fastify's own validation runs in step 4 (after this hook). Both can coexist — if oav rejects, Fastify's own validation never runs; if oav passes, Fastify's runs as usual. Authoring the same constraints in both places isn't recommended, but mixing them (oav for spec-driven validation, Fastify schemas for app-internal types) works.
API
validateRequests(validator, options?)
Returns a Fastify preValidationHookHandler.
| option | type | default |
| --------------- | ------------------------------------------ | ------------------------ |
| toHttpRequest | (request: FastifyRequest) => HttpRequest | httpRequestFromFastify |
| onError | (err, ctx) => void \| Promise<void> | renderProblemDetails |
onError may be async — the hook awaits it. Fastify awaits the returned promise, so thrown extractor errors and rejected onError promises propagate to Fastify's setErrorHandler automatically, no try/catch needed. The hook does not call reply.send() after onError returns — your callback owns the response (write to ctx.reply, or throw to delegate to Fastify's error handler).
Validation failures don't traverse Fastify's
setErrorHandlerby default. The defaultonError(renderProblemDetails) writes the response directly. If you want validation failures in your existing error pipeline, throw fromonError(Fastify routes throws tosetErrorHandler) or compose a logger beforerenderProblemDetails— see Add observability without changing the response.
httpRequestFromFastify(request)
Convert a FastifyRequest to oav's framework-agnostic HttpRequest shape. Read what's already on the request — body parsing is Fastify's responsibility (handled by content-type parsers before preValidation).
Header keys passed through (Fastify already lowercases per HTTP spec), path stripped of query string from request.url, query taken from request.query (Fastify parses it into an object), cookies read from request.cookies if @fastify/cookie populated them.
Returns a fresh HttpRequest. Top-level fields can be reassigned freely without affecting the original FastifyRequest — safe to spread ({ ...httpRequestFromFastify(req), body: {} }) or mutate in place.
Use this when you want to compose your own hook (e.g. validate inside a custom plugin) without re-implementing the extraction.
renderProblemDetails(err, ctx)
The default onError. RFC 9457 application/problem+json body (via toProblemDetails), status from httpStatusFor, Allow header from allowHeaderFor on 405.
Exported standalone so a custom onError can call it as the fallback path:
validateRequests(validator, {
onError: (err, ctx) => {
if (err.code === "security") return ctx.reply.code(401).send();
renderProblemDetails(err, ctx);
},
});Common patterns
Enable shape-only security checks (no auth middleware yet)
ValidatorOptions.validateSecurity is off by default — real apps run auth middleware (or hooks) upstream of the validator. During early dev (no auth wired yet) or with decorator-only auth that just attaches request.user, opt in:
const validator = createValidator(spec, { validateSecurity: true });
app.addHook("preValidation", validateRequests(validator));The check is shape-only — it confirms the declared credential is present, not that it's valid. Don't treat it as a substitute for auth middleware.
Custom error envelope
app.addHook(
"preValidation",
validateRequests(validator, {
onError: (err, ctx) => {
ctx.reply.code(httpStatusFor(err)).send({
message: formatSummary(err),
errors: collectIssues(err),
});
},
}),
);Forward to Fastify's setErrorHandler
Throw from onError — Fastify routes thrown errors to setErrorHandler:
app.addHook(
"preValidation",
validateRequests(validator, {
onError: (err) => {
throw new ValidationFailure(err);
},
}),
);
app.setErrorHandler((err, _request, reply) => {
if (err instanceof ValidationFailure) {
reply.code(422).send({ ... });
return;
}
// ... your existing error handler
});Add observability without changing the response
Validation failures don't reach your registered setErrorHandler by default (the hook terminates the request itself). To log every failure while keeping the default problem-details response, compose renderProblemDetails after your log call:
app.addHook(
"preValidation",
validateRequests(validator, {
onError: (err, ctx) => {
log.warn("validation failed", { url: ctx.request.url, code: err.code });
renderProblemDetails(err, ctx);
},
}),
);Use this whenever your existing error pipeline (Sentry, structured logger, request-id correlation) needs to see validation failures without changing the response shape.
Async onError (remote logging, dynamic config)
app.addHook(
"preValidation",
validateRequests(validator, {
onError: async (err, ctx) => {
await sentry.captureException(err);
renderProblemDetails(err, ctx);
},
}),
);The hook awaits the returned promise; rejections propagate to Fastify's setErrorHandler.
Coexisting with Fastify per-route schemas
Fastify's idiomatic per-route-schema pattern is independent of oav. The two can coexist in the same app:
- Use oav-fastify when the OpenAPI spec is the source of truth — for endpoints whose contract is published / contract-tested / shared with other languages or services.
- Use Fastify per-route schemas for app-internal types where you'd rather author the schema inline.
If both fire on the same route, oav's preValidation hook runs first; if it passes, Fastify's validation step runs next. Don't author the same constraints in both places.
Comparison with fastify-openapi-glue
fastify-openapi-glue reads an OpenAPI spec at startup and generates routes + handler stubs from it. oav-fastify is a different shape: it validates against the spec but you own the route declarations. Use fastify-openapi-glue if you want spec-driven scaffolding; use oav-fastify if your routes already exist and you want OpenAPI as the validation source of truth.
See also
oav-core—createValidator,ValidatorOptions,formatSummary,collectIssues,httpStatusFor,toProblemDetails.oav— batteries-included distribution of oav-core: YAML readers + theoavCLI.- The repo-root
INTEGRATION.md— broader recipes (security, file uploads, response validation, status mapping, type coercion, ignoring paths). - The repo-root
MIGRATION-FROM-EOV.md— porting fromexpress-openapi-validator.
