Automatically generate OpenAPI 3.0 documentation from Next.js projects, with support for Zod schemas and TypeScript types.
- ✅ Automatic OpenAPI 3.0 documentation generation from Next.js App Router
- ✅ Multiple schema types:
TypeScript,Zod,Drizzle-Zod, orcustom YAML/JSONfiles 🆕 - ✅ Mix schema sources simultaneously - perfect for gradual migrations 🆕
- ✅ JSDoc comments with intelligent parameter examples
- ✅ Multiple UI interfaces:
Scalar,Swagger,Redoc,Stoplight, andRapiDocavailable at/api-docsurl - ✅ Auto-detection of path parameters (e.g.,
/users/[id]/route.ts) - ✅ Intuitive CLI for quick setup and generation
- Scalar 🆕
- Swagger
- Redoc
- Stoplight Elements
- RapiDoc
Tip
You can use the --ui none option during initialization to skip UI setup if you only care about generating the OpenAPI documentation.
npm install next-openapi-gen --save-dev# Initialize OpenAPI configuration npx next-openapi-gen init --ui scalar --docs-url api-docs --schema zod # Generate OpenAPI documentation npx next-openapi-gen generateTip
Use the --output option in the init command to specify a custom output file for the template. Then you can use the --template option in the generate command to point to that file.
During initialization (npx next-openapi-gen init), a configuration file next.openapi.json will be created in the project's root directory:
{ "openapi": "3.0.0", "info": { "title": "Next.js API", "version": "1.0.0", "description": "API generated by next-openapi-gen" }, "servers": [ { "url": "http://localhost:3000", "description": "Local server" } ], "apiDir": "src/app/api", "schemaDir": "src/types", // or "src/schemas" for Zod schemas "schemaType": "zod", // or "typescript", or ["zod", "typescript"] for multiple "schemaFiles": [], // Optional: ["./schemas/models.yaml", "./schemas/api.json"] "outputFile": "openapi.json", "outputDir": "./public", "docsUrl": "/api-docs", "includeOpenApiRoutes": false, "ignoreRoutes": [], "debug": false }| Option | Description |
|---|---|
apiDir | Path to the API directory |
schemaDir | Path to the types/schemas directory |
schemaType | Schema type: "zod", "typescript", or ["zod", "typescript"] for multiple |
schemaFiles | Optional: Array of custom OpenAPI schema files (YAML/JSON) to include |
outputFile | Name of the OpenAPI output file |
outputDir | Directory where OpenAPI file will be generated (default: "./public") |
docsUrl | API documentation URL (for Swagger UI) |
includeOpenApiRoutes | Whether to include only routes with @openapi tag |
ignoreRoutes | Array of route patterns to exclude from documentation (supports wildcards) |
defaultResponseSet | Default error response set for all endpoints |
responseSets | Named sets of error response codes |
errorConfig | Error schema configuration |
debug | Enable detailed logging during generation |
// src/app/api/products/[id]/route.ts import { NextRequest, NextResponse } from "next/server"; import { z } from "zod"; export const ProductParams = z.object({ id: z.string().describe("Product ID"), }); export const ProductResponse = z.object({ id: z.string().describe("Product ID"), name: z.string().describe("Product name"), price: z.number().positive().describe("Product price"), }); /** * Get product information * @description Fetches detailed product information by ID * @pathParams ProductParams * @response ProductResponse * @openapi */ export async function GET( request: NextRequest, { params }: { params: { id: string } } ) { // Implementation... }// src/app/api/users/[id]/route.ts import { NextRequest, NextResponse } from "next/server"; type UserParams = { id: string; // User ID }; type UserResponse = { id: string; // User ID name: string; // Full name email: string; // Email address }; /** * Get user information * @description Fetches detailed user information by ID * @pathParams UserParams * @response UserResponse * @openapi */ export async function GET( request: NextRequest, { params }: { params: { id: string } } ) { // Implementation... }// src/db/schema.ts - Define your Drizzle table import { pgTable, serial, varchar, text } from "drizzle-orm/pg-core"; export const posts = pgTable("posts", { id: serial("id").primaryKey(), title: varchar("title", { length: 255 }).notNull(), content: text("content").notNull(), }); // src/schemas/post.ts - Generate Zod schema with drizzle-zod import { createInsertSchema, createSelectSchema } from "drizzle-zod"; import { posts } from "@/db/schema"; export const CreatePostSchema = createInsertSchema(posts, { title: (schema) => schema.title.min(5).max(255).describe("Post title"), content: (schema) => schema.content.min(10).describe("Post content"), }); export const PostResponseSchema = createSelectSchema(posts); // src/app/api/posts/route.ts - Use in your API route /** * Create a new post * @description Create a new blog post with Drizzle-Zod validation * @body CreatePostSchema * @response 201:PostResponseSchema * @openapi */ export async function POST(request: NextRequest) { const body = await request.json(); const validated = CreatePostSchema.parse(body); // Implementation... }| Tag | Description |
|---|---|
@description | Endpoint description |
@operationId | Custom operation ID (overrides auto-generated ID) |
@pathParams | Path parameters type/schema |
@params | Query parameters type/schema |
@body | Request body type/schema |
@bodyDescription | Request body description |
@response | Response type/schema with optional code and description (User, 201:User, User:Description, 201:User:Description) |
@responseDescription | Response description |
@responseSet | Override default response set (public, auth, none) |
@add | Add custom response codes (409:ConflictResponse, 429) |
@contentType | Request body content type (application/json, multipart/form-data) |
@auth | Authorization type (bearer, basic, apikey) |
@tag | Custom tag |
@deprecated | Marks the route as deprecated |
@openapi | Marks the route for inclusion in documentation (if includeOpenApiRoutes is enabled) |
@ignore | Excludes the route from OpenAPI documentation |
npx next-openapi-gen initThis command will generate following elements:
- Generate
next.openapi.jsonconfiguration file - Set up
ScalarUI for documentation display - Add
/api-docspage to display OpenAPI documentation - Configure
zodas the default schema tool
npx next-openapi-gen generateThis command will generate OpenAPI documentation based on your API code:
- Scan API directories for routes
- Analyze types/schemas
- Generate OpenAPI file (
openapi.json) in specified output directory (default:publicfolder) - Create Scalar/Swagger UI endpoint and page (if enabled)
To see API documenation go to http://localhost:3000/api-docs
// src/app/api/users/[id]/route.ts // Zod const UserParams = z.object({ id: z.string().describe("User ID"), }); // Or TypeScript type UserParams = { id: string; // User ID }; /** * @pathParams UserParams */ export async function GET() { // ... }// src/app/api/users/route.ts // Zod const UsersQueryParams = z.object({ page: z.number().optional().describe("Page number"), limit: z.number().optional().describe("Results per page"), search: z.string().optional().describe("Search phrase"), }); // Or TypeScript type UsersQueryParams = { page?: number; // Page number limit?: number; // Results per page search?: string; // Search phrase }; /** * @params UsersQueryParams */ export async function GET() { // ... }// src/app/api/users/route.ts // Zod const CreateUserBody = z.object({ name: z.string().describe("Full name"), email: z.string().email().describe("Email address"), password: z.string().min(8).describe("Password"), }); // Or TypeScript type CreateUserBody = { name: string; // Full name email: string; // Email address password: string; // Password }; /** * @body CreateUserBody * @bodyDescription User registration data including email and password */ export async function POST() { // ... }// src/app/api/users/route.ts // Zod const UserResponse = z.object({ id: z.string().describe("User ID"), name: z.string().describe("Full name"), email: z.string().email().describe("Email address"), createdAt: z.date().describe("Creation date"), }); // Or TypeScript type UserResponse = { id: string; // User ID name: string; // Full name email: string; // Email address createdAt: Date; // Creation date }; /** * @response UserResponse * @responseDescription Returns newly created user object */ export async function GET() { // ... } // Alternative formats with inline description /** * @response UserResponse:Returns user profile data */ export async function GET() { // ... } /** * @response 201:UserResponse:Returns newly created user */ export async function POST() { // ... } /** * @response 204:Empty:User successfully deleted */ export async function DELETE() { // ... }// src/app/api/protected/route.ts /** * @auth bearer */ export async function GET() { // ... }// src/app/api/v1/route.ts // Zod const UserSchema = z.object({ id: z.string(), name: z.string(), fullName: z.string().optional().describe("@deprecated Use name instead"), email: z.string().email(), }); // Or TypeScript type UserResponse = { id: string; name: string; /** @deprecated Use firstName and lastName instead */ fullName?: string; email: string; }; /** * @body UserSchema * @response UserResponse */ export async function GET() { // ... }// src/app/api/users/[id]/route.ts /** * Get user by ID * @operationId getUserById * @pathParams UserParams * @response UserResponse */ export async function GET() { // ... } // Generates: operationId: "getUserById" instead of auto-generated "get-users-{id}"// src/app/api/upload/route.ts // Zod const FileUploadSchema = z.object({ file: z.custom<File>().describe("Image file (PNG/JPG)"), description: z.string().optional().describe("File description"), category: z.string().describe("File category"), }); // Or TypeScript type FileUploadFormData = { file: File; description?: string; category: string; }; /** * @body FileUploadSchema * @contentType multipart/form-data */ export async function POST() { // ... }Configure reusable error sets in next.openapi.json:
{ "defaultResponseSet": "common", "responseSets": { "common": ["400", "401", "500"], "public": ["400", "500"], "auth": ["400", "401", "403", "500"] } }/** * Auto-default responses * @response UserResponse * @openapi */ export async function GET() {} // Generates: 200:UserResponse + common errors (400, 401, 500) /** * With custom description inline * @response UserResponse:Complete user profile information * @openapi */ export async function GET() {} // Generates: 200:UserResponse (with custom description) + common errors /** * Override response set * @response ProductResponse * @responseSet public * @openapi */ export async function GET() {} // Generates: 200:ProductResponse + public errors (400, 500) /** * Add custom responses with description * @response 201:UserResponse:User created successfully * @add 409:ConflictResponse * @openapi */ export async function POST() {} // Generates: 201:UserResponse (with custom description) + common errors + 409:ConflictResponse /** * Combine multiple sets * @response UserResponse * @responseSet auth,crud * @add 429:RateLimitResponse * @openapi */ export async function PUT() {} // Combines: auth + crud errors + custom 429{ "defaultResponseSet": "common", "responseSets": { "common": ["400", "500"], "auth": ["400", "401", "403", "500"], "public": ["400", "500"] }, "errorConfig": { "template": { "type": "object", "properties": { "error": { "type": "string", "example": "{{ERROR_MESSAGE}}" }, "code": { "type": "string", "example": "{{ERROR_CODE}}" } } }, "codes": { "400": { "description": "Bad Request", "variables": { "ERROR_MESSAGE": "Invalid request parameters", "ERROR_CODE": "BAD_REQUEST" } }, "401": { "description": "Unauthorized", "variables": { "ERROR_MESSAGE": "Authentication required", "ERROR_CODE": "UNAUTHORIZED" } }, "403": { "description": "Forbidden", "variables": { "ERROR_MESSAGE": "Access denied", "ERROR_CODE": "FORBIDDEN" } }, "404": { "description": "Not Found", "variables": { "ERROR_MESSAGE": "Resource not found", "ERROR_CODE": "NOT_FOUND" } }, "500": { "description": "Internal Server Error", "variables": { "ERROR_MESSAGE": "An unexpected error occurred", "ERROR_CODE": "INTERNAL_ERROR" } } } } }You can exclude routes from OpenAPI documentation in two ways:
Add the @ignore tag to any route you want to exclude:
// src/app/api/internal/route.ts /** * Internal route - not for documentation * @ignore */ export async function GET() { // This route will not appear in OpenAPI documentation }Add patterns to your next.openapi.json configuration file to exclude multiple routes at once:
{ "openapi": "3.0.0", "info": { "title": "Next.js API", "version": "1.0.0" }, "apiDir": "src/app/api", "ignoreRoutes": ["/internal/*", "/debug", "/admin/test/*"] }Pattern matching supports wildcards:
/internal/*- Ignores all routes under/internal//debug- Ignores only the/debugroute/admin/*/temp- Ignores routes like/admin/users/temp,/admin/posts/temp
The library automatically detects path parameters and generates documentation for them:
// src/app/api/users/[id]/posts/[postId]/route.ts // Will automatically detect 'id' and 'postId' parameters export async function GET() { // ... }If no type/schema is provided for path parameters, a default schema will be generated.
The library supports TypeScript generic types and automatically resolves them during documentation generation:
// src/app/api/llms/route.ts import { NextResponse } from "next/server"; // Define generic response wrapper type MyApiSuccessResponseBody<T> = T & { success: true; httpCode: string; }; // Define specific response data type LLMSResponse = { llms: Array<{ id: string; name: string; provider: string; isDefault: boolean; }>; }; /** * Get list of available LLMs * @description Get list of available LLMs with success wrapper * @response 200:MyApiSuccessResponseBody<LLMSResponse> * @openapi */ export async function GET() { return NextResponse.json({ success: true, httpCode: "200", llms: [ { id: "gpt-5", name: "GPT-5", provider: "OpenAI", isDefault: true, }, ], }); }The library generates intelligent examples for parameters based on their name:
| Parameter name | Example |
|---|---|
id, *Id | "123" or 123 |
slug | "example-slug" |
uuid | "123e4567-e89b-12d3-a456-426614174000" |
email | "user@example.com" |
name | "example-name" |
date | "2023-01-01" |
The library supports advanced Zod features such as:
// Zod validation chains are properly converted to OpenAPI schemas const EmailSchema = z .string() .email() .min(5) .max(100) .describe("Email address"); // Converts to OpenAPI with email format, minLength and maxLength// You can use TypeScript with Zod types import { z } from "zod"; const UserSchema = z.object({ id: z.string().uuid(), name: z.string().min(2), }); // Use z.infer to create a TypeScript type type User = z.infer<typeof UserSchema>; // The library will be able to recognize this schema by reference `UserSchema` or `User` type.The library automatically detects and expands Zod factory functions - any function that returns a Zod schema:
// Define reusable schema factory export function createPaginatedSchema<T extends z.ZodTypeAny>(dataSchema: T) { return z.object({ data: z.array(dataSchema).describe("Array of items"), pagination: z.object({ nextCursor: z.string().nullable(), hasMore: z.boolean(), limit: z.number().int().positive(), }), }); } // Use in your schemas - automatically expanded in OpenAPI export const PaginatedUsersSchema = createPaginatedSchema(UserSchema); export const PaginatedProductsSchema = createPaginatedSchema(ProductSchema);Factory functions work with any naming convention and support:
- Generic type parameters
- Inline schemas as arguments
- Imported schemas
- Multiple factory patterns in the same project
The library fully supports drizzle-zod for generating Zod schemas from Drizzle ORM table definitions. This provides a single source of truth for your database schema, validation, and API documentation.
Supported Functions:
createInsertSchema()- Generate schema for insertscreateSelectSchema()- Generate schema for selectscreateUpdateSchema()- Generate schema for updates
Features:
- ✅ Automatic field extraction from refinements
- ✅ Validation method conversion (min, max, email, url, etc.)
- ✅ Optional/nullable field detection
- ✅ Intelligent type mapping based on field names
- ✅ Full OpenAPI schema generation
Example:
import { createInsertSchema } from "drizzle-zod"; import { posts } from "@/db/schema"; export const CreatePostSchema = createInsertSchema(posts, { title: (schema) => schema.title.min(5).max(255), content: (schema) => schema.content.min(10), published: (schema) => schema.published.optional(), });See the complete Drizzle-Zod example for a full working implementation with a blog API.
Use multiple schema types simultaneously in a single project - perfect for gradual migrations, combining hand-written schemas with generated ones (protobuf, GraphQL), or using existing OpenAPI specs.
{ "schemaType": ["zod", "typescript"], "schemaDir": "./src/schemas", "schemaFiles": ["./schemas/external-api.yaml"] }- Custom files (highest) - from
schemaFilesarray - Zod schemas (medium) - if
"zod"inschemaType - TypeScript types (fallback) - if
"typescript"inschemaType
// Gradual TypeScript → Zod migration { "schemaType": ["zod", "typescript"] } // Zod + protobuf schemas { "schemaType": ["zod"], "schemaFiles": ["./proto/schemas.yaml"] } // Everything together { "schemaType": ["zod", "typescript"], "schemaFiles": ["./openapi-models.yaml"] }Custom schema files support YAML/JSON in OpenAPI 3.0 format. See next15-app-mixed-schemas for a complete working example.
Explore complete demo projects in the examples directory, covering integrations with Zod, TypeScript, Drizzle and documentation tools like Scalar and Swagger.
cd examples/next15-app-zod npm install npx next-openapi-gen generate npm run devThen open http://localhost:3000/api-docs to view the generated docs.
We welcome contributions! 🎉
Please read our Contributing Guide for details.
See CHANGELOG.md for release history and changes.
MIT




