Fastify
Fastify is a high-performance Node.js web framework. This guide shows how to generate an OpenAPI spec with to-openapi and serve it as a Fastify plugin.
Basic Setup
Generate your spec and register a route that serves it.
ts
import Fastify from "fastify";
import { openapi } from "to-openapi";
import { z } from "zod";
const spec = openapi({
info: {
title: "Fastify API",
version: "1.0.0",
},
servers: [{ url: "http://localhost:3000" }],
paths: {
"GET /articles": {
summary: "List articles",
tags: ["Articles"],
query: z.object({
page: z.number().optional(),
limit: z.number().optional(),
}),
200: z.array(
z.object({
id: z.string(),
title: z.string(),
content: z.string(),
published: z.boolean(),
})
),
},
"POST /articles": {
summary: "Create an article",
tags: ["Articles"],
body: z.object({
title: z.string(),
content: z.string(),
}),
201: z.object({
id: z.string(),
title: z.string(),
content: z.string(),
published: z.boolean(),
}),
400: { description: "Validation error" },
},
"GET /articles/{id}": {
summary: "Get an article by ID",
tags: ["Articles"],
params: z.object({
id: z.string(),
}),
200: z.object({
id: z.string(),
title: z.string(),
content: z.string(),
published: z.boolean(),
}),
404: { description: "Article not found" },
},
"PATCH /articles/{id}": {
summary: "Update an article",
tags: ["Articles"],
params: z.object({
id: z.string(),
}),
body: z.object({
title: z.string().optional(),
content: z.string().optional(),
published: z.boolean().optional(),
}),
200: z.object({
id: z.string(),
title: z.string(),
content: z.string(),
published: z.boolean(),
}),
404: { description: "Article not found" },
},
"DELETE /articles/{id}": {
summary: "Delete an article",
tags: ["Articles"],
params: z.object({
id: z.string(),
}),
204: { description: "Article deleted" },
404: { description: "Article not found" },
},
},
});
const app = Fastify({ logger: true });
// Serve the OpenAPI spec
app.get("/openapi.json", async () => {
return spec;
});
// Implement your routes
app.get("/articles", async () => {
return [];
});
app.post("/articles", async (request, reply) => {
const body = request.body as { title: string; content: string };
reply.status(201);
return { id: "1", ...body, published: false };
});
app.get("/articles/:id", async (request) => {
const { id } = request.params as { id: string };
return { id, title: "Hello World", content: "...", published: true };
});
app.patch("/articles/:id", async (request) => {
const { id } = request.params as { id: string };
const body = request.body as Record<string, unknown>;
return { id, title: "Hello World", content: "...", published: true, ...body };
});
app.delete("/articles/:id", async (_request, reply) => {
reply.status(204);
});
app.listen({ port: 3000 });As a Fastify Plugin
Encapsulate the OpenAPI spec serving logic as a reusable Fastify plugin.
ts
import Fastify, { type FastifyInstance } from "fastify";
import fp from "fastify-plugin";
import { openapi, type ToOpenapiDefinition } from "to-openapi";
// Plugin that generates and serves the OpenAPI spec
function openapiPlugin(
fastify: FastifyInstance,
opts: { definition: ToOpenapiDefinition; path?: string },
done: () => void,
) {
const spec = openapi(opts.definition);
const servePath = opts.path ?? "/openapi.json";
fastify.get(servePath, async () => {
return spec;
});
done();
}
export const openapiSpec = fp(openapiPlugin);Then use it in your application:
ts
import Fastify from "fastify";
import { z } from "zod";
import { openapiSpec } from "./openapi-plugin.js";
const app = Fastify({ logger: true });
app.register(openapiSpec, {
definition: {
info: {
title: "Fastify API",
version: "1.0.0",
},
paths: {
"GET /items": {
summary: "List items",
200: z.array(z.object({ id: z.string(), name: z.string() })),
},
"POST /items": {
summary: "Create an item",
body: z.object({ name: z.string() }),
201: z.object({ id: z.string(), name: z.string() }),
},
},
},
path: "/openapi.json",
});
app.get("/items", async () => {
return [];
});
app.post("/items", async (request, reply) => {
const body = request.body as { name: string };
reply.status(201);
return { id: "1", ...body };
});
app.listen({ port: 3000 });Using the Class-Based API
The OpenAPI class builder is useful when routes are registered dynamically or across multiple plugin files.
ts
import Fastify from "fastify";
import { OpenAPI } from "to-openapi";
import { z } from "zod";
const api = new OpenAPI({
info: {
title: "Fastify API",
version: "1.0.0",
},
servers: [{ url: "http://localhost:3000" }],
});
const app = Fastify({ logger: true });
// Register routes with both Fastify and the OpenAPI builder
api.route("get", "/comments", {
summary: "List comments",
tags: ["Comments"],
query: z.object({
articleId: z.string().optional(),
}),
200: z.array(
z.object({
id: z.string(),
articleId: z.string(),
text: z.string(),
})
),
});
app.get("/comments", async () => {
return [];
});
api.route("post", "/comments", {
summary: "Add a comment",
tags: ["Comments"],
body: z.object({
articleId: z.string(),
text: z.string(),
}),
201: z.object({
id: z.string(),
articleId: z.string(),
text: z.string(),
}),
});
app.post("/comments", async (request, reply) => {
const body = request.body as { articleId: string; text: string };
reply.status(201);
return { id: "1", ...body };
});
// Serve the spec (call .document() to generate)
app.get("/openapi.json", async () => {
return api.document();
});
app.listen({ port: 3000 });Fastify's Built-in OpenAPI Support
Fastify has its own OpenAPI integration through @fastify/swagger, which generates specs from Fastify's JSON Schema route definitions.
Use @fastify/swagger when:
- You already use Fastify's built-in JSON Schema validation.
- You want spec generation tightly coupled with Fastify's route lifecycle.
Use to-openapi when:
- You use schema libraries like Zod, ArkType, or Valibot via Standard Schema instead of raw JSON Schema.
- You need the plugin system for cross-cutting concerns (bearer auth, auto tags, error responses).
- You want to merge specs from multiple services using
merge(). - You share schema definitions across frameworks or services that are not all Fastify.