Next.js integration for @restdocs with App Router support
npm install @restdocs/nextjsTest-driven API documentation for Next.js 14+ App Router.
- ✅ Test-Driven Documentation: Documentation generated from actual tests ensures accuracy
- 🔄 Hybrid Mode: Quick feedback during development, test verification in production
- 🎨 Interactive Dev UI: Built-in Swagger UI for exploring and testing endpoints
- 📦 Zero Config: Minimal setup, works out of the box
- 🎯 Type-Safe: Full TypeScript support with type inference
- 🚀 Next.js 14+ App Router: First-class support for modern Next.js
``bash`
npm install @restdocs/nextjs --save-devor
pnpm add -D @restdocs/nextjs
`ts
// next.config.ts
import { withRestDocs } from '@restdocs/nextjs/config';
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
reactStrictMode: true,
};
export default withRestDocs({
...nextConfig,
// RestDocs configuration (optional)
restdocs: {
mode: 'hybrid', // 'test-driven' | 'development' | 'hybrid'
enabled: process.env.NODE_ENV === 'development',
path: '/api/__restdocs',
},
});
`
`ts
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { api, field } from '@restdocs/nextjs';
export async function POST(request: NextRequest) {
const body = await request.json();
// Document the API
api.document('POST /api/users', {
description: 'Create a new user',
tags: ['Users'],
request: {
email: field.email().required(),
name: field.string().required(),
},
response: {
id: field.uuid(),
email: field.email(),
name: field.string(),
},
statusCode: 201,
});
const user = { id: crypto.randomUUID(), ...body };
return NextResponse.json(user, { status: 201 });
}
`
Start your Next.js development server and visit:
``
http://localhost:3000/api/__restdocs
You'll see an interactive Swagger UI with all your documented endpoints.
Documentation is only generated from tests. This ensures documentation always matches actual behavior.
`ts`
// next.config.ts
restdocs: {
mode: 'test-driven'
}
Documentation can be defined in route handlers for rapid prototyping.
`ts`
// next.config.js
restdocs: {
mode: 'development'
}
Best of both worlds:
- Route-based docs show up immediately in Dev UI (marked as ⚠️ unverified)
- Test-based docs are marked as ✅ verified
- CI requires all endpoints to be verified by tests
`ts`
// next.config.js
restdocs: {
mode: 'hybrid',
hybrid: {
allowRouteDocumentation: true,
requireTests: true, // CI will fail if docs aren't verified
showDocumentationSource: true
}
}
`ts
// __tests__/api/users.test.ts
import { api, field } from '@restdocs/nextjs';
import { POST } from '@/app/api/users/route';
describe('POST /api/users', () => {
it('should create a new user', async () => {
// Document in test (verified documentation)
api.document('POST /api/users', {
description: 'Create a new user',
tags: ['Users'],
request: {
email: field.email().required(),
name: field.string().required(),
},
response: {
id: field.uuid(),
email: field.email(),
name: field.string(),
},
statusCode: 201,
});
const request = new Request('http://localhost:3000/api/users', {
method: 'POST',
body: JSON.stringify({
email: 'test@example.com',
name: 'Test User',
}),
});
const response = await POST(request as any);
expect(response.status).toBe(201);
// Test passes → documentation marked as verified ✅
});
});
`
@restdocs provides a fluent API for defining schemas:
`ts
import { field } from '@restdocs/nextjs';
field.string() // String field
field.number() // Number field
field.integer() // Integer field
field.boolean() // Boolean field
field.uuid() // UUID string
field.email() // Email string
field.url() // URL string
field.datetime() // ISO 8601 datetime string
field.array(field.string()) // Array of strings
field.object({ // Object with properties
name: field.string(),
age: field.integer()
})
// Modifiers
field.string().required() // Required field
field.string().optional() // Optional field
field.string().minLength(5) // Min length
field.string().maxLength(100) // Max length
field.number().min(0).max(100) // Min/max value
field.string().enum(['a', 'b', 'c']) // Enum values
field.string().default('default') // Default value
field.string().description('...') // Description
`
RestDocs provides first-class support for documenting file upload endpoints with the field.file() builder:
`ts
// app/api/upload/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { api, field } from '@restdocs/nextjs';
export async function POST(request: NextRequest) {
api.document('POST /api/upload', {
description: 'Upload a file with metadata',
tags: ['Files'],
headers: {
Authorization: field.string().required(),
},
request: {
file: field.file()
.required()
.acceptedTypes(['image/png', 'image/jpeg', 'application/pdf'])
.maxSize(5 1024 1024) // 5MB
.description('File to upload'),
title: field.string().required(),
description: field.string().optional(),
},
response: {
id: field.uuid(),
filename: field.string(),
size: field.integer(),
type: field.string(),
uploadedAt: field.datetime(),
},
statusCode: 201,
});
const formData = await request.formData();
const file = formData.get('file') as File;
// ... handle upload
}
`
`ts`
field.file()
.required() // Mark as required
.optional() // Mark as optional
.format('binary') // binary (default) or base64
.acceptedTypes('image/*') // Single MIME type
.acceptedTypes(['image/png', 'image/jpeg']) // Multiple MIME types
.maxSize(5 1024 1024) // Max file size in bytes
.minSize(1024) // Min file size in bytes
.description('Upload your profile picture') // Description
`ts`
api.document('POST /api/gallery', {
request: {
images: field.array(field.file())
.minItems(1)
.maxItems(10)
.description('Upload up to 10 images'),
caption: field.string().optional(),
},
response: {
galleryId: field.uuid(),
imageCount: field.integer(),
},
});
For base64-encoded files:
`ts`
api.document('POST /api/upload-base64', {
request: {
file: field.file()
.format('base64')
.acceptedTypes('image/*')
.description('Base64-encoded image'),
},
});
The file fields automatically generate correct OpenAPI 3.0 specifications:
`json`
{
"requestBody": {
"required": true,
"content": {
"multipart/form-data": {
"schema": {
"type": "object",
"properties": {
"file": {
"type": "string",
"format": "binary",
"description": "File to upload",
"x-content-type": ["image/png", "image/jpeg"],
"x-max-size": 5242880
}
},
"required": ["file"]
}
}
}
}
}
`ts
// __tests__/api/upload.test.ts
it('should upload file with FormData', async () => {
api.document('POST /api/upload', {
request: {
file: field.file()
.required()
.acceptedTypes(['image/png', 'image/jpeg'])
.maxSize(5 1024 1024),
title: field.string().required(),
},
response: {
id: field.uuid(),
filename: field.string(),
},
statusCode: 201,
});
const formData = new FormData();
const blob = new Blob(['content'], { type: 'image/png' });
const file = new File([blob], 'photo.png', { type: 'image/png' });
formData.append('file', file);
formData.append('title', 'Profile Picture');
const request = new NextRequest('http://localhost:3000/api/upload', {
method: 'POST',
body: formData,
});
const response = await POST(request);
expect(response.status).toBe(201);
});
`
RestDocs automatically generates:
1. OpenAPI 3.0 Spec: GET /api/__restdocs/openapi.jsonGET /api/__restdocs
2. Interactive Swagger UI: GET /api/__restdocs/stats
3. Statistics:
Validate documentation in your CI pipeline:
`ts
// scripts/validate-docs.ts
import { api } from '@restdocs/nextjs';
const result = api.validate();
if (!result.valid) {
console.error('Documentation validation failed:');
result.errors?.forEach(err => {
console.error(- ${err.endpoint}: ${err.message}); ${err.suggestion}
console.error();`
});
process.exit(1);
}
`ts
interface RestDocsConfig {
// Documentation mode
mode?: 'test-driven' | 'development' | 'hybrid'; // default: 'hybrid'
// Enable/disable RestDocs
enabled?: boolean; // default: NODE_ENV === 'development'
// Dev UI path
path?: string; // default: '/api/__restdocs'
// Output directory for generated files
outputDir?: string; // default: './docs/api'
// Hybrid mode configuration
hybrid?: {
allowRouteDocumentation?: boolean; // default: true
requireTests?: boolean; // default: true
showDocumentationSource?: boolean; // default: true
};
}
``
MIT