npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@pixengine/middleware-nextjs

v0.1.0

Published

Next.js App Router handler for PixEngine image optimization

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/core

Quick 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

  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

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

Links