npm package: @restdocs/nextjs
Description: Next.js integration for @restdocs with App Router support
Author: Jae Song Choi
Latest version: 0.3.2

# @restdocs/nextjs

Test-driven API documentation for Next.js 14+ App Router.

## Features

- ✅ **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

## Installation

```bash
npm install @restdocs/nextjs --save-dev
# or
pnpm add -D @restdocs/nextjs
```

## Quick Start

### 1. Configure Next.js

```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',
    },
});
```

### 2. Document Your API Routes

```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 });
}
```

### 3. Access the Dev UI

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 Modes

### Test-Driven (Recommended)

Documentation is only generated from tests. This ensures documentation always matches actual behavior.

```ts
// next.config.ts
restdocs: {
    mode: 'test-driven'
}
```

### Development Mode

Documentation can be defined in route handlers for rapid prototyping.

```ts
// next.config.js
restdocs: {
    mode: 'development'
}
```

### Hybrid Mode (Default)

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
    }
}
```

## Writing Tests

```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 ✅
    });
});
```

## Field Builders

@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
```

## File Upload / FormData Support

RestDocs provides first-class support for documenting file upload endpoints with the `field.file()` builder:

### Basic File Upload

```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
}
```

### File Field Options

```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
```

### Multiple File Upload

```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(),
  },
});
```

### Base64 Encoding

For base64-encoded files:

```ts
api.document('POST /api/upload-base64', {
  request: {
    file: field.file()
      .format('base64')
      .acceptedTypes('image/*')
      .description('Base64-encoded image'),
  },
});
```

### Generated OpenAPI Spec

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"]
        }
      }
    }
  }
}
```

### Testing File Uploads

```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);
});
```

## Generated Outputs

RestDocs automatically generates:

1. **OpenAPI 3.0 Spec**: `GET /api/__restdocs/openapi.json`
2. **Interactive Swagger UI**: `GET /api/__restdocs`
3. **Statistics**: `GET /api/__restdocs/stats`

## CI Validation

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}`);
        console.error(`  ${err.suggestion}`);
    });
    process.exit(1);
}
```

## Configuration Options

```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
    };
}
```

## License

MIT