Automatically generate OpenAPI 3.0 documentation from Next.js projects, with support for Zod schemas and TypeScript types.
- ✅ Automatic OpenAPI documentation generation from Next.js code
- ✅ Support for Next.js App Router (including
/api/users/[id]/route.tsroutes) - ✅ TypeScript types support
- ✅ Zod schemas support
- ✅ Drizzle-Zod support - Generate schemas from Drizzle ORM tables 🆕
- ✅ JSDoc comments support
- ✅ Multiple UI interfaces:
Scalar,Swagger,Redoc,StoplightandRapidocavailable at/api-docsurl - ✅ Path parameters detection (
/users/{id}) - ✅ Intelligent parameter examples
- ✅ Intuitive CLI for initialization and documentation generation
- Scalar 🆕
- Swagger
- Redoc
- Stoplight Elements
- RapiDoc
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 generateDuring 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" for TypeScript types
"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" or "typescript" |
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 |
@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/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 |
"[email protected]" |
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 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.
This repository includes several complete example projects:
| Example | Description | Features |
|---|---|---|
| next15-app-zod | Zod schemas example | Users, Products, Orders API with Zod validation |
| next15-app-drizzle-zod | Drizzle-Zod integration 🆕 | Blog API with Drizzle ORM + drizzle-zod |
| next15-app-typescript | TypeScript types | API with pure TypeScript type definitions |
| next15-app-scalar | Scalar UI | Modern API documentation interface |
| next15-app-swagger | Swagger UI | Classic Swagger documentation |
cd examples/next15-app-drizzle-zod
npm install
npm run openapi:generate
npm run devVisit http://localhost:3000/api-docs to see the generated documentation.
- Drizzle-Zod Example - Complete example with Drizzle ORM integration
- Drizzle ORM - TypeScript ORM for SQL databases
- drizzle-zod - Zod schema generator for Drizzle
- Zod Documentation - TypeScript-first schema validation
- Next.js Documentation - React framework documentation
- OpenAPI Specification - OpenAPI 3.0 spec
- Scalar Documentation - Modern API documentation UI
MIT




