Interactive API docs help developers adopt your API faster. Here is how to generate them automatically from an OpenAPI spec.
Define Your OpenAPI Spec
// lib/openapi.ts
import { OpenAPIV3 } from "openapi-types";
export const openApiDocument: OpenAPIV3.Document = {
openapi: "3.0.3",
info: {
title: "My API",
version: "1.0.0",
description: "REST API for managing resources",
},
servers: [
{
url: "https://api.example.com/v1",
description: "Production",
},
{
url: "http://localhost:3000/api/v1",
description: "Local development",
},
],
paths: {
"/users": {
get: {
summary: "List users",
operationId: "listUsers",
tags: ["Users"],
parameters: [
{
name: "page",
in: "query",
schema: { type: "integer", default: 1 },
},
{
name: "limit",
in: "query",
schema: { type: "integer", default: 20, maximum: 100 },
},
],
responses: {
"200": {
description: "Paginated list of users",
content: {
"application/json": {
schema: {
type: "object",
properties: {
data: {
type: "array",
items: { $ref: "#/components/schemas/User" },
},
pagination: { $ref: "#/components/schemas/Pagination" },
},
},
},
},
},
},
},
post: {
summary: "Create a user",
operationId: "createUser",
tags: ["Users"],
requestBody: {
required: true,
content: {
"application/json": {
schema: { $ref: "#/components/schemas/CreateUser" },
},
},
},
responses: {
"201": {
description: "User created",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/User" },
},
},
},
"422": {
description: "Validation error",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/Error" },
},
},
},
},
},
},
},
components: {
schemas: {
User: {
type: "object",
properties: {
id: { type: "string", format: "uuid" },
name: { type: "string" },
email: { type: "string", format: "email" },
createdAt: { type: "string", format: "date-time" },
},
required: ["id", "name", "email", "createdAt"],
},
CreateUser: {
type: "object",
properties: {
name: { type: "string", minLength: 1, maxLength: 100 },
email: { type: "string", format: "email" },
},
required: ["name", "email"],
},
Pagination: {
type: "object",
properties: {
page: { type: "integer" },
limit: { type: "integer" },
total: { type: "integer" },
totalPages: { type: "integer" },
},
},
Error: {
type: "object",
properties: {
message: { type: "string" },
code: { type: "string" },
details: {
type: "array",
items: {
type: "object",
properties: {
field: { type: "string" },
message: { type: "string" },
},
},
},
},
},
},
securitySchemes: {
bearerAuth: {
type: "http",
scheme: "bearer",
bearerFormat: "JWT",
},
},
},
security: [{ bearerAuth: [] }],
};
Serve the OpenAPI JSON
// app/api/openapi/route.ts
import { NextResponse } from "next/server";
import { openApiDocument } from "@/lib/openapi";
export async function GET() {
return NextResponse.json(openApiDocument);
}
Option 1: Scalar API Docs
Scalar provides a modern, beautiful API reference UI.
pnpm add @scalar/nextjs-api-reference
// app/docs/api/page.tsx
import { ApiReference } from "@scalar/nextjs-api-reference";
export default function ApiDocsPage() {
return (
<ApiReference
configuration={{
url: "/api/openapi",
theme: "kepler",
layout: "modern",
hideModels: false,
searchHotKey: "k",
}}
/>
);
}
Option 2: Custom Documentation UI
Build your own docs from the OpenAPI spec.
// app/docs/api/page.tsx
import { openApiDocument } from "@/lib/openapi";
import type { OpenAPIV3 } from "openapi-types";
function MethodBadge({ method }: { method: string }) {
const colors: Record<string, string> = {
get: "bg-blue-100 text-blue-800",
post: "bg-green-100 text-green-800",
put: "bg-yellow-100 text-yellow-800",
patch: "bg-orange-100 text-orange-800",
delete: "bg-red-100 text-red-800",
};
return (
<span
className={`rounded px-2 py-1 text-xs font-bold uppercase ${colors[method] ?? "bg-gray-100 text-gray-800"}`}
>
{method}
</span>
);
}
function EndpointCard({
path,
method,
operation,
}: {
path: string;
method: string;
operation: OpenAPIV3.OperationObject;
}) {
return (
<div className="border rounded-lg p-4">
<div className="flex items-center gap-3 mb-2">
<MethodBadge method={method} />
<code className="text-sm font-mono">{path}</code>
</div>
<p className="text-sm text-muted-foreground">{operation.summary}</p>
{operation.parameters && (
<div className="mt-3">
<h4 className="text-xs font-semibold uppercase text-muted-foreground mb-1">
Parameters
</h4>
<div className="space-y-1">
{(operation.parameters as OpenAPIV3.ParameterObject[]).map((param) => (
<div key={param.name} className="flex items-center gap-2 text-sm">
<code className="text-xs bg-muted px-1 rounded">{param.name}</code>
<span className="text-xs text-muted-foreground">{param.in}</span>
{param.required && (
<span className="text-xs text-red-500">required</span>
)}
</div>
))}
</div>
</div>
)}
</div>
);
}
export default function ApiDocsPage() {
const { paths, info } = openApiDocument;
const groupedByTag: Record<
string,
{ path: string; method: string; operation: OpenAPIV3.OperationObject }[]
> = {};
if (paths) {
for (const [path, methods] of Object.entries(paths)) {
for (const [method, operation] of Object.entries(methods as Record<string, OpenAPIV3.OperationObject>)) {
const tag = operation.tags?.[0] ?? "Other";
if (!groupedByTag[tag]) groupedByTag[tag] = [];
groupedByTag[tag].push({ path, method, operation });
}
}
}
return (
<div className="max-w-4xl mx-auto py-12 px-4">
<h1 className="text-3xl font-bold mb-2">{info.title}</h1>
<p className="text-muted-foreground mb-8">{info.description}</p>
{Object.entries(groupedByTag).map(([tag, endpoints]) => (
<section key={tag} className="mb-10">
<h2 className="text-xl font-semibold mb-4">{tag}</h2>
<div className="space-y-4">
{endpoints.map(({ path, method, operation }) => (
<EndpointCard
key={`${method}-${path}`}
path={path}
method={method}
operation={operation}
/>
))}
</div>
</section>
))}
</div>
);
}
Validate Routes Against the Spec
// lib/openapi-validator.ts
import { openApiDocument } from "./openapi";
import { z } from "zod";
export function getRequestSchema(operationId: string) {
const paths = openApiDocument.paths ?? {};
for (const methods of Object.values(paths)) {
for (const operation of Object.values(methods as Record<string, any>)) {
if (operation.operationId === operationId) {
return operation.requestBody?.content?.["application/json"]?.schema;
}
}
}
return null;
}
// Usage in API routes
// app/api/v1/users/route.ts
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
const CreateUserSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
});
export async function POST(request: NextRequest) {
const body = await request.json();
const parsed = CreateUserSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json(
{
message: "Validation error",
code: "VALIDATION_ERROR",
details: parsed.error.issues.map((issue) => ({
field: issue.path.join("."),
message: issue.message,
})),
},
{ status: 422 }
);
}
// Create user...
return NextResponse.json({ id: "...", ...parsed.data }, { status: 201 });
}
Need API Documentation for Your Product?
We build developer-friendly API documentation integrated into your marketing site. Contact us to create docs that drive adoption.