ProvenPin Install

Fastify 5 OpenAPI docs generated with @fastify/swagger: working versions (tested 2026-10-15)

Generate an OpenAPI 3 document from Fastify 5 route schemas with @fastify/swagger, using shared $ref schemas, tags and security, then serve and check it with inject().

passed3 passed · 0 failed2026-10-15 17:10 UTC
Status
passed
Last passed
2026-10-15 17:10 UTC
Tests
3 / 3
Language
TypeScript
Runtime
node v22.23.3
pinned versions4
fastify@5.12.7@fastify/swagger@9.9.2@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 38775053802

The code

Exactly the files that passed. Your agent can pull them with get_recipe("fastify-swagger-openapi") (install), or see them on GitHub.

package.json
{
  "name": "fastify-swagger-openapi",
  "private": true,
  "type": "module",
  "scripts": {
    "test": "node --import tsx --test test/*.test.ts"
  },
  "dependencies": {
    "fastify": "5.12.7",
    "@fastify/swagger": "9.9.2"
  },
  "devDependencies": {
    "@types/node": "26.6.5",
    "tsx": "4.23.15"
  }
}
src/app.ts
import Fastify from "fastify";
import swagger from "@fastify/swagger";

export async function buildApp() {
  const app = Fastify();

  // Register swagger before routes so it sees every route schema.
  await app.register(swagger, {
    openapi: {
      openapi: "3.1.0",
      info: { title: "Parts API", version: "1.0.0" },
      components: { securitySchemes: { bearerAuth: { type: "http", scheme: "bearer" } } },
    },
    // Name shared schemas after their $id instead of the default def-0, def-1...
    refResolver: { buildLocalReference: (json, _baseUri, _fragment, i) => String(json.$id ?? `def-${i}`) },
  });

  app.addSchema({
    $id: "Part",
    type: "object",
    required: ["id", "name"],
    properties: { id: { type: "integer" }, name: { type: "string" }, price: { type: "number" } },
  });

  const parts = [{ id: 1, name: "bolt", price: 0.25 }];

  app.get("/parts", {
    schema: {
      tags: ["parts"],
      summary: "List parts",
      querystring: { type: "object", properties: { q: { type: "string" } } },
      response: { 200: { type: "array", items: { $ref: "Part#" } } },
    },
  }, async (req) => {
    const { q } = req.query as { q?: string };
    return q ? parts.filter((p) => p.name.includes(q)) : parts;
  });

  app.post("/parts", {
    schema: {
      tags: ["parts"],
      summary: "Create a part",
      security: [{ bearerAuth: [] }],
      body: { type: "object", required: ["name"], properties: { name: { type: "string", minLength: 1 }, price: { type: "number" } } },
      response: { 201: { $ref: "Part#" } },
    },
  }, async (req, reply) => {
    const body = req.body as { name: string; price?: number };
    const part = { id: parts.length + 1, ...body };
    parts.push(part);
    return reply.code(201).send(part);
  });

  app.get("/openapi.json", { schema: { hide: true } }, async () => app.swagger());

  await app.ready();
  return app;
}
test/app.test.ts
import { test } from "node:test";
import assert from "node:assert/strict";
import { buildApp } from "../src/app.ts";

test("generates an OpenAPI 3.1 document from route schemas", async () => {
  const app = await buildApp();
  const doc = app.swagger() as any;
  assert.equal(doc.openapi, "3.1.0");
  assert.equal(doc.info.title, "Parts API");
  assert.deepEqual(Object.keys(doc.paths).sort(), ["/parts"]);
  assert.equal(doc.paths["/parts"].get.summary, "List parts");
  assert.deepEqual(doc.paths["/parts"].post.security, [{ bearerAuth: [] }]);
  assert.equal(doc.paths["/parts"].get.parameters[0].name, "q");
  assert.deepEqual(doc.components.schemas.Part.required, ["id", "name"]);
  assert.equal(doc.paths["/parts"].post.responses["201"].content["application/json"].schema.$ref, "#/components/schemas/Part");
  assert.equal(doc.components.securitySchemes.bearerAuth.scheme, "bearer");
  await app.close();
});

test("serves the document and hides the docs route itself", async () => {
  const app = await buildApp();
  const res = await app.inject({ method: "GET", url: "/openapi.json" });
  assert.equal(res.statusCode, 200);
  const doc = res.json();
  assert.equal(doc.paths["/openapi.json"], undefined);
  await app.close();
});

test("the same schemas validate requests", async () => {
  const app = await buildApp();
  const bad = await app.inject({ method: "POST", url: "/parts", payload: { name: "" } });
  assert.equal(bad.statusCode, 400);
  const ok = await app.inject({ method: "POST", url: "/parts", payload: { name: "nut", price: 0.1 } });
  assert.equal(ok.statusCode, 201);
  assert.deepEqual(ok.json(), { id: 2, name: "nut", price: 0.1 });
  await app.close();
});

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.