npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@aahoughton/oav-express4

v1.1.1

Published

Express 4 adapter for @aahoughton/oav-core. Ships a request-validator middleware factory plus standalone helpers (httpRequestFromExpress, renderProblemDetails) for callers that want to compose their own.

Downloads

393

Readme

oav-express4

Express 4 adapter for oav-core — a request-validator middleware factory plus standalone helpers (httpRequestFromExpress, renderProblemDetails) for callers composing their own middleware.

Thin: this package re-exports nothing from oav-core. You install both. The adapter declares oav-core as a regular dependency, so a single npm install @aahoughton/oav-express4 pulls oav-core along; or install oav instead if you want YAML readers and the CLI.

Sibling packages: oav-express5, oav-fastify. Same export names, option shapes, and defaults; only the framework-typed argument differs.

Migrating from express-openapi-validator? See MIGRATION-FROM-EOV.md for behavior differences (path-label /params//path/, errorCode namespacing, status mapping) and a worked porting walkthrough.

Install

# JSON specs only
npm install @aahoughton/oav-core @aahoughton/oav-express4 express

# YAML specs + CLI (oav transitively provides oav-core)
npm install @aahoughton/oav @aahoughton/oav-express4 express

express is a peer dep — your app's existing install satisfies it.

YAML specs. oav-core is JSON-only by design (zero runtime deps). If your spec is YAML, either install oav instead — it bundles the YAML readers and the CLI — or install yaml separately and parse the spec yourself before passing the parsed object to createValidator.

Quick start

import express from "express";
import { createValidator } from "@aahoughton/oav-core";
import { validateRequests } from "@aahoughton/oav-express4";

const validator = createValidator(spec);

const app = express();
app.use(express.json()); // ← MUST run before validateRequests
app.use(validateRequests(validator));

app.post("/pets", (req, res) => res.json({ 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.

Body parser ordering matters. express.json() (or your equivalent) must run before validateRequests(...), otherwise req.body is undefined and the validator emits body required for every request — a misleading error that points at the schema, not at the missing parser. Same for cookie-parser if your spec validates cookies. Any middleware that populates req.body with a parsed object satisfies oav — express.json(), custom streaming parsers, body-parser, fastify's bridge, app-specific middleware all work the same way.

Empty-body normalisation. Some parsers (streaming variants, custom multi-format setups) leave req.body === undefined even after they run, for empty {}-equivalent payloads. When that happens, required-field checks short-circuit on the missing body — empty submissions pass validation. Normalise via toHttpRequest:

import { httpRequestFromExpress, validateRequests } from "@aahoughton/oav-express4";

app.use(
  validateRequests(validator, {
    toHttpRequest: (req) => ({ ...httpRequestFromExpress(req), body: req.body ?? {} }),
  }),
);

Stock express.json() populates an empty body to {} and doesn't hit this — but migrators inheriting alternative parsers (e.g. body-parser streaming mode) often do.

API

validateRequests(validator, options?)

Returns an Express 4 RequestHandler.

| option | type | default | | --------------- | ------------------------------------- | ------------------------ | | toHttpRequest | (req: Request) => HttpRequest | httpRequestFromExpress | | onError | (err, ctx) => void \| Promise<void> | renderProblemDetails |

onError may be async — the middleware awaits it. If it throws or rejects, the error is forwarded via next(err) so the host's error middleware sees it. The middleware does not call next() after onError returns — your callback owns the response (write to ctx.res, or call ctx.next(err) to delegate).

Validation failures don't traverse Express's error chain by default. The default onError (renderProblemDetails) writes the response directly. If you're migrating from express-openapi-validator (which emits validation failures as HttpError through next(err)), your existing error middleware won't see oav's failures unless you forward them — see Forward to Express's error middleware below. Same goes for observability: see Add observability without changing the response.

httpRequestFromExpress(req)

Convert an Express 4 Request to oav's framework-agnostic HttpRequest shape. Read what's already on req — body parsing is the host app's responsibility.

Header keys lowercased, path stripped of query string, cookies read from req.cookies if present.

Returns a fresh HttpRequest. Top-level fields can be reassigned freely without affecting the original Express req — safe to spread ({ ...httpRequestFromExpress(req), body: {} }) or mutate in place. The values it references (req.body, req.headers) are still the originals; deep mutation would still leak, but reassignment doesn't.

Use this when you want to compose your own middleware (e.g. validate inside an existing custom wrapper) 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.res.status(401).end();
    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 upstream of the validator, so by the time validateRequests runs the credential has already been verified. During early dev (no auth wired yet) or with decorator-only auth that just attaches req.user, opt in:

const validator = createValidator(spec, { validateSecurity: true });
app.use(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.

Per-scheme auth dispatch (the eov securityHandlers shape)

eov's securityHandlers is a per-scheme dispatch table — you supply an auth function per declared scheme and eov calls it. oav-express4 doesn't ship this as a helper, but the recipe is small. Mount it as middleware before validateRequests:

import type { Request } from "express";
import { createValidator } from "@aahoughton/oav-core";

type SchemeHandler = (req: Request, scopes: string[]) => Promise<boolean>;

const handlers: Record<string, SchemeHandler> = {
  bearerAuth: async (req, scopes) => {
    const token = req.headers.authorization?.replace(/^Bearer /, "");
    return verifyJwt(token, scopes);
  },
  apiKeyAuth: async (req) => {
    const key = req.header("x-api-key");
    return Boolean(key) && (await verifyApiKey(key));
  },
};

app.use(async (req, res, next) => {
  const op = validator.getOperation({ method: req.method, path: req.path });
  const requirements = op?.operation.security ?? spec.security ?? [];
  if (requirements.length === 0) return next();
  for (const requirement of requirements) {
    let allPass = true;
    for (const [scheme, scopes] of Object.entries(requirement)) {
      const handler = handlers[scheme];
      if (!handler || !(await handler(req, scopes))) {
        allPass = false;
        break;
      }
    }
    if (allPass) return next();
  }
  res.status(401).type("application/problem+json").json({
    type: "about:blank",
    title: "Unauthorized",
    status: 401,
    detail: "no security requirement satisfied",
  });
});

app.use(validateRequests(validator)); // shape check off by default; redundant given the dispatcher above

OpenAPI semantics: each requirement object is AND across its scheme keys; the outer array is OR across requirements. The recipe walks them accordingly.

If multiple projects end up copying this recipe, that's the signal to harvest into a dispatchSecurity(...) helper export — not yet.

Skip validation for paths the spec doesn't declare

The validator owns this — pass it ignorePaths or ignoreUndocumented at construction. See ValidatorOptions in oav-core for the contract.

const validator = createValidator(spec, {
  ignorePaths: (p) => p.startsWith("/internal/"),
});
app.use(validateRequests(validator));

Custom error envelope

app.use(
  validateRequests(validator, {
    onError: (err, ctx) => {
      ctx.res.status(httpStatusFor(err)).json({
        message: formatSummary(err),
        errors: collectIssues(err),
      });
    },
  }),
);

Forward to Express's error middleware

app.use(
  validateRequests(validator, {
    onError: (err, ctx) => ctx.next(new ValidationFailure(err)),
  }),
);

app.use((err, _req, res, _next) => {
  if (err instanceof ValidationFailure) {
    res.status(422).json({ ... });
    return;
  }
  // ... your existing error handler
});

Add observability without changing the response

Validation failures don't reach your registered Express error middleware by default (the middleware terminates the request itself). To log every failure while keeping the default problem-details response, compose renderProblemDetails after your log call:

app.use(
  validateRequests(validator, {
    onError: (err, ctx) => {
      log.warn("validation failed", { path: ctx.req.path, 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.use(
  validateRequests(validator, {
    onError: async (err, ctx) => {
      await sentry.captureException(err);
      renderProblemDetails(err, ctx);
    },
  }),
);

The middleware awaits the returned promise; rejections route to next(err).

Per-route mounting

validateRequests(...) is route-aware (it derives the operation from method+path). Mount it once at the app level — per-route mounting is redundant and may cause double-validation under nested routers.

Global validator + per-route multer (file uploads)

When the validator is mounted globally and one or a few routes accept file uploads via multer, mount multer at the route prefix that needs it (upstream of the global validator) and use toHttpRequest to synthesize the spec-shaped body from req.files:

import multer from "multer";
import { httpRequestFromExpress, validateRequests } from "@aahoughton/oav-express4";

const upload = multer({ storage: multer.memoryStorage() });
app.use("/uploads", upload.any());

app.use(
  validateRequests(validator, {
    toHttpRequest: (req) => {
      const httpReq = httpRequestFromExpress(req);
      const files = req.files as Express.Multer.File[] | undefined;
      if (files && files.length > 0) {
        httpReq.body = files.length === 1 ? files[0]?.buffer : files.map((f) => f.buffer);
      }
      return httpReq;
    },
  }),
);

toHttpRequest is the general "reshape what oav sees" seam — synthesizing body from files, normalizing empty bodies, merging headers from an upstream proxy, anything that lives above the extraction layer. The empty-body normalization recipe higher in this README and this multer recipe are two examples of the same pattern.

For per-route inline multer (validator called from inside the route handler) and the full multer recipe with text-field reassembly, see the INTEGRATION.md file uploads section.

See also

  • oav-corecreateValidator, ValidatorOptions, formatSummary, collectIssues, httpStatusFor, toProblemDetails.
  • oav — batteries-included distribution of oav-core: YAML readers + the oav CLI.
  • 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 from express-openapi-validator.