ProvenPin Install

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
pinned versions7
express@5.3.0express-openapi-validator@5.6.2supertest@7.3.1@types/express@5.0.6@types/supertest@7.2.1@types/node@26.6.5tsx@4.23.15
run it
npm install --no-audit --no-fund --ignore-scripts=false
npm test
test environment
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.

package.json
{
  "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"
  }
}
src/app.ts
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;
}
test/app.test.ts
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.

terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.