UNPKG

next-openapi-gen

Version:

Automatically generate OpenAPI 3.0 documentation from Next.js projects, with support for Zod schemas and TypeScript types.

977 lines (784 loc) 27.3 kB
# next-openapi-gen Automatically generate OpenAPI 3.0 documentation from Next.js projects, with support for Zod schemas and TypeScript types. ## Features - Automatic OpenAPI 3.0 documentation generation from Next.js App Router - Multiple schema types: `TypeScript`, `Zod`, `Drizzle-Zod`, or `custom YAML/JSON` files 🆕 - Mix schema sources simultaneously - perfect for gradual migrations 🆕 - JSDoc comments with intelligent parameter examples - Multiple UI interfaces: `Scalar`, `Swagger`, `Redoc`, `Stoplight`, and `RapiDoc` available at `/api-docs` url - Auto-detection of path parameters (e.g., `/users/[id]/route.ts`) - Intuitive CLI for quick setup and generation ## Supported interfaces - Scalar 💡(default) - Swagger - Redoc - Stoplight Elements - RapiDoc ## Installation ```bash npm install next-openapi-gen --save-dev ``` ## Quick Start ```bash # Initialize OpenAPI configuration npx next-openapi-gen init # Generate OpenAPI documentation npx next-openapi-gen generate ``` > [!TIP] > Scalar UI and Zod are set by default ### Init Command Options | Option | Choices | Default | Description | |--------|---------|---------|-------------| | `--ui` | `scalar`, `swagger`, `redoc`, `stoplight`, `rapidoc`, `none` | `scalar` | UI framework for API docs | | `--schema` | `zod`, `typescript` | `zod` | Schema validation tool | | `--docs-url` | any string | `api-docs` | URL path for documentation page | | `--output` | any path | `next.openapi.json` | Output file for OpenAPI template | > [!TIP] > Use `--ui none` to skip UI setup and only generate the OpenAPI specification file. ## Configuration During initialization (`npx next-openapi-gen init`), a configuration file `next.openapi.json` will be created in the project's root directory: ```json { "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 } ``` ### Configuration Options | 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 | ## Documenting Your API ### With Zod Schemas ```typescript // 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... } ``` ### With TypeScript Types ```typescript // 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... } ``` ### With Drizzle-Zod ```typescript // 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... } ``` ## JSDoc Documentation Tags | Tag | Description | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------ | | `@description` | Endpoint description | | `@operationId` | Custom operation ID (overrides auto-generated ID) | | `@pathParams` | Path parameters type/schema | | `@params` | Query parameters type/schema (use `@queryParams` if you have prettier-plugin-jsdoc conflicts) | | `@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 | ## CLI Usage ### 1. Initialization ```bash npx next-openapi-gen init ``` This command will generate following elements: - Generate `next.openapi.json` configuration file - Set up `Scalar` UI for documentation display - Add `/api-docs` page to display OpenAPI documentation - Configure `zod` as the default schema tool ### 2. Generate Documentation ```bash npx next-openapi-gen generate ``` This 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: `public` folder) - Create Scalar/Swagger UI endpoint and page (if enabled) ### 3. View API Documentation To see API documenation go to `http://localhost:3000/api-docs` ## Examples ### Path Parameters ```typescript // 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() { // ... } ``` ### Query Parameters ```typescript // 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() { // ... } ``` ### Request Body ```typescript // 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() { // ... } ``` ### Response ```typescript // 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() { // ... } ``` ### Authorization ```typescript // src/app/api/protected/route.ts /** * @auth bearer */ export async function GET() { // ... } ``` ### Deprecated ```typescript // 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() { // ... } ``` ### Custom Operation ID ```typescript // 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}" ``` ### File Uploads / Multipart Form Data ```typescript // 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() { // ... } ``` ## Response Management ### Zero Config + Response Sets Configure reusable error sets in `next.openapi.json`: ```json { "defaultResponseSet": "common", "responseSets": { "common": ["400", "401", "500"], "public": ["400", "500"], "auth": ["400", "401", "403", "500"] } } ``` ### Usage Examples ```typescript /** * 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 ``` ### Error Schema Configuration #### Define consistent error schemas using templates: ```json { "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" } } } } } ``` ## Ignoring Routes You can exclude routes from OpenAPI documentation in two ways: ### Using @ignore Tag Add the `@ignore` tag to any route you want to exclude: ```typescript // src/app/api/internal/route.ts /** * Internal route - not for documentation * @ignore */ export async function GET() { // This route will not appear in OpenAPI documentation } ``` ### Using ignoreRoutes Configuration Add patterns to your `next.openapi.json` configuration file to exclude multiple routes at once: ```json { "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 `/debug` route - `/admin/*/temp` - Ignores routes like `/admin/users/temp`, `/admin/posts/temp` ## Advanced Usage ### Automatic Path Parameter Detection The library automatically detects path parameters and generates documentation for them: ```typescript // 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. ### TypeScript Generics Support The library supports TypeScript generic types and automatically resolves them during documentation generation: ```typescript // 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, }, ], }); } ``` ### Intelligent Examples 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"` | ## Advanced Zod Features The library supports advanced Zod features such as: ### Validation Chains ```typescript // 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 ``` ### Type Aliases with z.infer ```typescript // 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. ``` ### Factory Functions (Schema Generators) The library automatically detects and expands Zod factory functions - any function that returns a Zod schema: ```typescript // 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 ### Schema Composition with `.extend()` Zod's `.extend()` method allows you to build upon existing schemas: ```typescript // src/schemas/user.ts // Base user schema export const BaseUserSchema = z.object({ id: z.string().uuid().describe("User ID"), email: z.string().email().describe("Email address"), }); // Extend with additional fields export const UserProfileSchema = BaseUserSchema.extend({ name: z.string().describe("Full name"), bio: z.string().optional().describe("User biography"), }); // Multiple levels of extension export const AdminUserSchema = UserProfileSchema.extend({ role: z.enum(["admin", "moderator"]).describe("Admin role"), permissions: z.array(z.string()).describe("Permission list"), }); export const UserIdParams = z.object({ id: z.string().uuid().describe("User ID"), }); // src/app/api/users/[id]/route.ts /** * Get user profile * @pathParams UserIdParams * @response UserProfileSchema * @openapi */ export async function GET( request: NextRequest, { params }: { params: { id: string } } ) { // Returns: { id, email, name, bio? } } ``` ### Drizzle-Zod Support 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 inserts - `createSelectSchema()` - Generate schema for selects - `createUpdateSchema()` - 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:** ```typescript 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](./examples/next15-app-drizzle-zod) for a full working implementation with a blog API. ## Multiple Schema Types Support 🆕 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. ### Configuration ```json { "schemaType": ["zod", "typescript"], "schemaDir": "./src/schemas", "schemaFiles": ["./schemas/external-api.yaml"] } ``` ### Schema Resolution Priority 1. **Custom files** (highest) - from `schemaFiles` array 2. **Zod schemas** (medium) - if `"zod"` in `schemaType` 3. **TypeScript types** (fallback) - if `"typescript"` in `schemaType` ### Common Use Cases ```json // 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](./examples/next15-app-mixed-schemas)** for a complete working example. ## Examples Explore complete demo projects in the **[examples](./examples/)** directory, covering integrations with Zod, TypeScript, Drizzle and documentation tools like Scalar and Swagger. ### 🚀 Run an Example ```bash cd examples/next15-app-zod npm install npx next-openapi-gen generate npm run dev ``` Then open `http://localhost:3000/api-docs` to view the generated docs. ## Available UI providers <div align="center"> <table> <thead> <th>Scalar</th> <th>Swagger</th> <th>Redoc</th> <th>Stoplight Elements</th> <th>RapiDoc</th> </thead> <tbody> <tr> <td> <img width="320" alt="scalar" src="https://raw.githubusercontent.com/tazo90/next-openapi-gen/refs/heads/main/assets/scalar.png" alt-text="scalar"> </td> <td> <img width="320" alt="swagger" src="https://raw.githubusercontent.com/tazo90/next-openapi-gen/refs/heads/main/assets/swagger.png" alt-text="swagger"> </td> <td> <img width="320" alt="redoc" src="https://raw.githubusercontent.com/tazo90/next-openapi-gen/refs/heads/main/assets/redoc.png" alt-text="redoc"> </td> <td> <img width="320" alt="stoplight" src="https://raw.githubusercontent.com/tazo90/next-openapi-gen/refs/heads/main/assets/stoplight.png" alt-text="stoplight"> </td> <td> <img width="320" alt="rapidoc" src="https://raw.githubusercontent.com/tazo90/next-openapi-gen/refs/heads/main/assets/rapidoc.png" alt-text="rapidoc"> </td> </tr> </tbody> </table> </div> ## Contributing We welcome contributions! 🎉 Please read our [Contributing Guide](CONTRIBUTING.md) for details. ## Changelog See [CHANGELOG.md](CHANGELOG.md) for release history and changes. ## License MIT