express-openapi-validator request and response validation: working versions (tested 2026-10-18)
Validate an Express app against an inline OpenAPI 3 spec using express-openapi-validator, returning validation errors as JSON and testing valid and invalid requests offline with supertest.
passed5 passed · 0 failed2026-10-18 03:58 UTC- Status
- passed
- Last passed
- 2026-10-18 03:58 UTC
- Tests
- 5 / 5
- Language
- TypeScript
- Runtime
- node v22.23.3
npm install --no-audit --no-fund --ignore-scripts=false npm test
node v22.23.3 linux 6.17.0-1022-azure x64 runner: github-actions 39095442401
The code
Exactly the files that passed. Your agent can pull them with get_recipe("express-openapi-validator-spec") (install), or see them on GitHub.
{
"name": "express-openapi-validator-spec",
"private": true,
"type": "module",
"scripts": {
"test": "node --import tsx --test test/*.test.ts"
},
"dependencies": {
"express": "5.3.0",
"express-openapi-validator": "5.6.2"
},
"devDependencies": {
"@types/express": "5.0.6",
"@types/node": "26.6.5",
"@types/supertest": "7.2.1",
"supertest": "7.3.1",
"tsx": "4.23.15"
}
}
import express, { type NextFunction, type Request, type Response } from "express";
import OpenApiValidator from "express-openapi-validator";
export const apiSpec = {
openapi: "3.0.3",
info: { title: "Items API", version: "1.0.0" },
paths: {
"/items": {
post: {
operationId: "createItem",
requestBody: {
required: true,
content: {
"application/json": {
schema: { $ref: "#/components/schemas/NewItem" },
},
},
},
responses: {
"201": {
description: "Item created",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/Item" },
},
},
},
},
},
},
"/items/{id}": {
get: {
operationId: "getItem",
parameters: [
{
name: "id",
in: "path",
required: true,
schema: { type: "string", pattern: "^[a-z0-9-]+$" },
},
{
name: "verbose",
in: "query",
required: false,
schema: { type: "boolean" },
},
],
responses: {
"200": {
description: "An item",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/Item" },
},
},
},
},
},
},
},
components: {
schemas: {
Item: {
type: "object",
required: ["id", "name", "quantity"],
properties: {
id: { type: "string" },
name: { type: "string" },
quantity: { type: "integer", minimum: 0 },
},
},
NewItem: {
type: "object",
required: ["name", "quantity"],
properties: {
name: { type: "string", minLength: 1 },
quantity: { type: "integer", minimum: 0 },
},
},
},
},
} as const;
export function createApp() {
const app = express();
app.use(express.json());
app.use(
OpenApiValidator.middleware({
apiSpec: apiSpec as any,
validateRequests: true,
validateResponses: true,
}),
);
app.post("/items", (req, res) => {
const { name, quantity } = req.body as { name: string; quantity: number };
res.status(201).json({ id: "generated", name, quantity });
});
app.get("/items/:id", (req, res) => {
const verbose = req.query.verbose === true || req.query.verbose === "true";
res.status(200).json({
id: req.params.id,
name: verbose ? `Item ${req.params.id}` : "Item",
quantity: 1,
});
});
app.use((err: any, _req: Request, res: Response, _next: NextFunction) => {
const status = typeof err.status === "number" ? err.status : 500;
res.status(status).json({
message: err.message ?? "Internal Server Error",
errors: Array.isArray(err.errors) ? err.errors : [],
});
});
return app;
}
import { test } from "node:test";
import assert from "node:assert/strict";
import request from "supertest";
import { createApp } from "../src/app.ts";
test("GET valid path and query returns 200", async () => {
const app = createApp();
const res = await request(app).get("/items/abc-1").query({ verbose: "true" });
assert.equal(res.status, 200);
assert.equal(res.body.id, "abc-1");
assert.equal(res.body.name, "Item abc-1");
assert.equal(res.body.quantity, 1);
});
test("GET invalid path param returns 400 with details", async () => {
const app = createApp();
const res = await request(app).get("/items/BADID");
assert.equal(res.status, 400);
assert.ok(Array.isArray(res.body.errors));
assert.ok(res.body.errors.length > 0);
});
test("GET invalid query returns 400 with details", async () => {
const app = createApp();
const res = await request(app).get("/items/abc-1").query({ verbose: "maybe" });
assert.equal(res.status, 400);
assert.ok(Array.isArray(res.body.errors));
assert.ok(res.body.errors.length > 0);
});
test("POST valid JSON body returns 201", async () => {
const app = createApp();
const res = await request(app)
.post("/items")
.send({ name: "Widget", quantity: 3 });
assert.equal(res.status, 201);
assert.equal(res.body.name, "Widget");
assert.equal(res.body.quantity, 3);
});
test("POST invalid JSON body returns 400 with details", async () => {
const app = createApp();
const res = await request(app)
.post("/items")
.send({ name: "", quantity: -1 });
assert.equal(res.status, 400);
assert.ok(Array.isArray(res.body.errors));
assert.ok(res.body.errors.length > 0);
});
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.
Related stacks
cors express preflight
Configure cors with express to reflect allowed origins, answer OPTIONS preflight with Access-Control-Allow-* headers, and deny a disallowed origin, tested offline with supertest.
express 5 async error handling
Use express 5 with supertest to forward rejected async handlers to a JSON error middleware, with router-level params, a JSON 404 handler and offline tests.
express-validator request checks with supertest
Validate Express JSON bodies and query params with express-validator, returning field errors, tested with supertest offline.
http-errors with Express error middleware
Express routes that throw http-errors and reject asynchronously, mapped by a central middleware to RFC 7807 problem details with expose-aware messages and a 500 fallback, tested offline with supertest.
All Express recipesAll express-openapi-validator recipesAll supertest recipes