npm package: @pixengine/middleware-nextjs
Description: Next.js App Router handler for PixEngine image optimization
Author: PixEngine Team
Latest version: 0.3.0

# @pixengine/middleware-nextjs

**English** | [한국어](README_KO.md)

Next.js App Router handler for PixEngine image optimization.

## Installation

```bash
npm install @pixengine/middleware-nextjs @pixengine/core
# or
pnpm add @pixengine/middleware-nextjs @pixengine/core
# or
yarn add @pixengine/middleware-nextjs @pixengine/core
```

## Quick Start

```typescript
// app/api/upload/route.ts
import { pixEngineHandler } from '@pixengine/middleware-nextjs';
import { SharpEngine } from '@pixengine/adapter-engine-sharp';
import { LocalStorage } from '@pixengine/adapter-storage-local';

export const POST = pixEngineHandler({
  engine: new SharpEngine(),
  storage: new LocalStorage({
    baseDir: './public/uploads',
    baseUrl: '/uploads',
  }),
});
```

**Response:**
```json
{
  "original": {
    "width": 1920,
    "height": 1080,
    "format": "jpeg",
    "bytes": 245760
  },
  "variants": [
    {
      "key": "variants/photo_400w.webp",
      "url": "/uploads/variants/photo_400w.webp",
      "width": 400,
      "height": 225,
      "format": "webp",
      "bytes": 8420
    },
    {
      "key": "variants/photo_800w.webp",
      "url": "/uploads/variants/photo_800w.webp",
      "width": 800,
      "height": 450,
      "format": "webp",
      "bytes": 24680
    },
    {
      "key": "variants/photo_1200w.webp",
      "url": "/uploads/variants/photo_1200w.webp",
      "width": 1200,
      "height": 675,
      "format": "webp",
      "bytes": 48920
    }
  ]
}
```

## Features

- 🚀 **Next.js App Router**: Built for Next.js 14+ App Router
- 📤 **FormData Support**: Works with native FormData file uploads
- 🎨 **Automatic Optimization**: Generates responsive image variants automatically
- 📦 **Default Policy**: Sensible defaults (400w, 800w, 1200w WebP images)
- ⚙️ **Customizable**: Override policy for custom image variants
- 🔒 **Type-Safe**: Full TypeScript support
- ✅ **Auto JSON Response**: Returns manifest directly to client
- ⚡ **Edge Runtime Compatible**: Works with Edge Runtime (when using compatible adapters)

## API

### `pixEngineHandler(config)`

Factory function that creates a Next.js Route Handler for image optimization.

#### Parameters

```typescript
interface PixEngineHandlerConfig {
  engine: TransformEngine;    // Required: Image processing engine
  storage: StorageAdapter;     // Required: Storage adapter
  policy?: Policy;             // Optional: Custom variant policy
}
```

**Required:**
- `engine: TransformEngine` - Image processing engine (e.g., `SharpEngine`)
- `storage: StorageAdapter` - Storage adapter (e.g., `LocalStorage`, S3, etc.)

**Optional:**
- `policy?: Policy` - Custom policy function to define image variants

#### Returns

`(request: Request) => Promise<Response>` - Next.js Route Handler function

### Default Policy

The handler provides a default responsive image policy:

```typescript
export const defaultPolicy: Policy = (ctx) => ({
  variants: [
    { width: 400, format: 'webp', quality: 80 },
    { width: 800, format: 'webp', quality: 85 },
    { width: 1200, format: 'webp', quality: 90 },
  ],
});
```

This generates three WebP variants at different widths, suitable for responsive web images.

## Usage Examples

### Basic Usage with Default Policy

```typescript
// app/api/upload/route.ts
import { pixEngineHandler } from '@pixengine/middleware-nextjs';
import { SharpEngine } from '@pixengine/adapter-engine-sharp';
import { LocalStorage } from '@pixengine/adapter-storage-local';

export const POST = pixEngineHandler({
  engine: new SharpEngine(),
  storage: new LocalStorage({
    baseDir: './public/uploads',
    baseUrl: '/uploads',
  }),
});
```

### Custom Policy

```typescript
// app/api/upload/route.ts
import { pixEngineHandler } from '@pixengine/middleware-nextjs';
import { SharpEngine } from '@pixengine/adapter-engine-sharp';
import { LocalStorage } from '@pixengine/adapter-storage-local';

export const POST = pixEngineHandler({
  engine: new SharpEngine(),
  storage: new LocalStorage({
    baseDir: './public/uploads',
    baseUrl: '/uploads',
  }),
  policy: (ctx) => ({
    variants: [
      { width: 200, format: 'webp', quality: 75 },
      { width: 600, format: 'webp', quality: 80 },
      { width: 1000, format: 'jpeg', quality: 85 },
    ],
  }),
});
```

### Context-Based Policy

```typescript
// app/api/upload/route.ts
import { pixEngineHandler } from '@pixengine/middleware-nextjs';
import { SharpEngine } from '@pixengine/adapter-engine-sharp';
import { LocalStorage } from '@pixengine/adapter-storage-local';

export const POST = pixEngineHandler({
  engine: new SharpEngine(),
  storage: new LocalStorage({
    baseDir: './public/uploads',
    baseUrl: '/uploads',
  }),
  policy: (ctx) => {
    // Access original image metadata
    const { width, height, format } = ctx.original;

    // Generate variants based on original size
    if (width > 2000) {
      return {
        variants: [
          { width: 800, format: 'webp', quality: 80 },
          { width: 1600, format: 'webp', quality: 85 },
          { width: 2400, format: 'webp', quality: 90 },
        ],
      };
    }

    // Smaller originals get fewer variants
    return {
      variants: [
        { width: 400, format: 'webp', quality: 80 },
        { width: 800, format: 'webp', quality: 85 },
      ],
    };
  },
});
```

### Client-Side Upload Example

```typescript
// app/upload/page.tsx
'use client';

import { useState } from 'react';

export default function UploadPage() {
  const [manifest, setManifest] = useState(null);

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);

    const response = await fetch('/api/upload', {
      method: 'POST',
      body: formData,
    });

    const data = await response.json();
    setManifest(data);
  };

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <input type="file" name="image" accept="image/*" />
        <button type="submit">Upload</button>
      </form>

      {manifest && (
        <div>
          <h2>Optimized Images:</h2>
          {manifest.variants.map((variant) => (
            <img key={variant.url} src={variant.url} alt="Optimized" />
          ))}
        </div>
      )}
    </div>
  );
}
```

## Error Handling

The handler handles errors automatically:

### 400 Bad Request
Returned when no file is uploaded:

```json
{
  "error": "No file uploaded"
}
```

### 500 Internal Server Error
Returned when optimization fails:

```json
{
  "error": "Image optimization failed",
  "message": "Unsupported image format"
}
```

### Custom Error Handling

You can wrap the handler to add custom error handling:

```typescript
// app/api/upload/route.ts
import { pixEngineHandler } from '@pixengine/middleware-nextjs';
import { SharpEngine } from '@pixengine/adapter-engine-sharp';
import { LocalStorage } from '@pixengine/adapter-storage-local';

const handler = pixEngineHandler({
  engine: new SharpEngine(),
  storage: new LocalStorage({
    baseDir: './public/uploads',
    baseUrl: '/uploads',
  }),
});

export async function POST(request: Request) {
  try {
    return await handler(request);
  } catch (error) {
    console.error('Upload error:', error);
    return Response.json(
      { error: 'Upload failed', details: error.message },
      { status: 500 }
    );
  }
}
```

## Requirements

- **Node.js**: >= 18.0.0
- **Next.js**: ^14.0.0 || ^15.0.0
- **PixEngine Core**: @pixengine/core
- **Transform Engine**: e.g., @pixengine/adapter-engine-sharp
- **Storage Adapter**: e.g., @pixengine/adapter-storage-local

## How It Works

1. **Client uploads file** via FormData
2. **Next.js Route Handler** receives Request
3. **Extracts file** from FormData
4. **Calls `optimize()`** with configured engine, storage, and policy
5. **Returns manifest** as JSON Response automatically

```
Client → FormData → Next.js Route Handler → optimize() → Storage → JSON Response
```

## Integration with Other Storage Adapters

### AWS S3 Storage

```typescript
// app/api/upload/route.ts
import { pixEngineHandler } from '@pixengine/middleware-nextjs';
import { SharpEngine } from '@pixengine/adapter-engine-sharp';
import { S3Storage } from '@pixengine/adapter-storage-s3';

export const POST = pixEngineHandler({
  engine: new SharpEngine(),
  storage: new S3Storage({
    bucket: 'my-images',
    region: 'us-east-1',
    baseUrl: 'https://cdn.example.com',
  }),
});
```

### Cloudflare R2

```typescript
// app/api/upload/route.ts
import { pixEngineHandler } from '@pixengine/middleware-nextjs';
import { SharpEngine } from '@pixengine/adapter-engine-sharp';
import { R2Storage } from '@pixengine/adapter-storage-r2';

export const POST = pixEngineHandler({
  engine: new SharpEngine(),
  storage: new R2Storage({
    accountId: 'your-account-id',
    accessKeyId: process.env.R2_ACCESS_KEY_ID,
    secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,
    bucket: 'my-images',
  }),
});
```

## Best Practices

### 1. Use Environment Variables

```typescript
// app/api/upload/route.ts
import { pixEngineHandler } from '@pixengine/middleware-nextjs';
import { SharpEngine } from '@pixengine/adapter-engine-sharp';
import { LocalStorage } from '@pixengine/adapter-storage-local';

export const POST = pixEngineHandler({
  engine: new SharpEngine(),
  storage: new LocalStorage({
    baseDir: process.env.UPLOAD_DIR || './public/uploads',
    baseUrl: process.env.BASE_URL || '/uploads',
  }),
});
```

### 2. Add File Size Limits

```typescript
// app/api/upload/route.ts
import { pixEngineHandler } from '@pixengine/middleware-nextjs';
import { SharpEngine } from '@pixengine/adapter-engine-sharp';
import { LocalStorage } from '@pixengine/adapter-storage-local';

const handler = pixEngineHandler({
  engine: new SharpEngine(),
  storage: new LocalStorage({
    baseDir: './public/uploads',
    baseUrl: '/uploads',
  }),
});

export async function POST(request: Request) {
  const contentLength = request.headers.get('content-length');
  const MAX_SIZE = 10 * 1024 * 1024; // 10MB

  if (contentLength && parseInt(contentLength) > MAX_SIZE) {
    return Response.json(
      { error: 'File too large', maxSize: '10MB' },
      { status: 413 }
    );
  }

  return handler(request);
}
```

### 3. Validate File Types

```typescript
// app/api/upload/route.ts
import { pixEngineHandler } from '@pixengine/middleware-nextjs';
import { SharpEngine } from '@pixengine/adapter-engine-sharp';
import { LocalStorage } from '@pixengine/adapter-storage-local';

const handler = pixEngineHandler({
  engine: new SharpEngine(),
  storage: new LocalStorage({
    baseDir: './public/uploads',
    baseUrl: '/uploads',
  }),
});

export async function POST(request: Request) {
  const formData = await request.formData();
  const file = formData.get('image');

  if (!file || !(file instanceof File)) {
    return Response.json({ error: 'No file uploaded' }, { status: 400 });
  }

  const allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
  if (!allowedTypes.includes(file.type)) {
    return Response.json(
      { error: 'Invalid file type', allowedTypes },
      { status: 400 }
    );
  }

  return handler(request);
}
```

### 4. Set Appropriate CORS Headers

```typescript
// app/api/upload/route.ts
import { pixEngineHandler } from '@pixengine/middleware-nextjs';
import { SharpEngine } from '@pixengine/adapter-engine-sharp';
import { LocalStorage } from '@pixengine/adapter-storage-local';

const handler = pixEngineHandler({
  engine: new SharpEngine(),
  storage: new LocalStorage({
    baseDir: './public/uploads',
    baseUrl: '/uploads',
  }),
});

export async function POST(request: Request) {
  const response = await handler(request);

  // Add CORS headers if needed
  response.headers.set('Access-Control-Allow-Origin', '*');
  response.headers.set('Access-Control-Allow-Methods', 'POST, OPTIONS');

  return response;
}

export async function OPTIONS() {
  return new Response(null, {
    status: 204,
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': 'POST, OPTIONS',
      'Access-Control-Allow-Headers': 'Content-Type',
    },
  });
}
```

### 5. Use Edge Runtime (with compatible adapters)

```typescript
// app/api/upload/route.ts
import { pixEngineHandler } from '@pixengine/middleware-nextjs';
import { SharpEngine } from '@pixengine/adapter-engine-sharp';
import { S3Storage } from '@pixengine/adapter-storage-s3';

export const runtime = 'edge';

export const POST = pixEngineHandler({
  engine: new SharpEngine(), // Note: Sharp may not work on Edge, use compatible engine
  storage: new S3Storage({
    bucket: 'my-images',
    region: 'us-east-1',
    baseUrl: 'https://cdn.example.com',
  }),
});
```

## Comparison with Express Middleware

| Feature | Next.js Handler | Express Middleware |
|---------|----------------|-------------------|
| API | Route Handler | Middleware function |
| Request Type | Web API `Request` | Express `Request` |
| Response Type | Web API `Response` | Express `Response` |
| File Upload | FormData | Multer |
| Runtime | Node.js / Edge | Node.js only |
| Framework | Next.js 14+ | Express 4+ / 5+ |

## License

MIT © PixEngine Team

## Links

- [PixEngine Core](https://www.npmjs.com/package/@pixengine/core)
- [Sharp Engine Adapter](https://www.npmjs.com/package/@pixengine/adapter-engine-sharp)
- [Local Storage Adapter](https://www.npmjs.com/package/@pixengine/adapter-storage-local)
- [Express Middleware](https://www.npmjs.com/package/@pixengine/middleware-express)
- [GitHub Repository](https://github.com/pixengine/pixengine)
- [Issue Tracker](https://github.com/pixengine/pixengine/issues)