@pixengine/middleware-nextjs
v0.1.0
Published
Next.js App Router handler for PixEngine image optimization
Maintainers
Readme
@pixengine/middleware-nextjs
English | 한국어
Next.js App Router handler for PixEngine image optimization.
Installation
npm install @pixengine/middleware-nextjs @pixengine/core
# or
pnpm add @pixengine/middleware-nextjs @pixengine/core
# or
yarn add @pixengine/middleware-nextjs @pixengine/coreQuick Start
// 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:
{
"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
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:
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
// 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
// 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
// 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
// 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:
{
"error": "No file uploaded"
}500 Internal Server Error
Returned when optimization fails:
{
"error": "Image optimization failed",
"message": "Unsupported image format"
}Custom Error Handling
You can wrap the handler to add custom error handling:
// 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
- Client uploads file via FormData
- Next.js Route Handler receives Request
- Extracts file from FormData
- Calls
optimize()with configured engine, storage, and policy - Returns manifest as JSON Response automatically
Client → FormData → Next.js Route Handler → optimize() → Storage → JSON ResponseIntegration with Other Storage Adapters
AWS S3 Storage
// 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
// 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
// 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
// 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
// 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
// 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)
// 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
