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

@code-plate/next-image-serve

v1.0.0

Published

Configurable Next.js App Router route handler for safely serving local images with ETag/304 support, in-memory LRU caching, and streaming for large files.

Readme

@code-plate/next-image-serve

A configurable Next.js App Router route handler for serving local images with in-memory LRU caching, ETag/304 support, and automatic streaming for large files.

Instead of writing this route handler from scratch for every project, you get one function — createImageHandler(options) — that returns a ready-to-export GET handler, with almost every behavior overridable.

Features

  • Path traversal protection — every segment is sanitized and the resolved path is checked against basePath
  • Two-layer caching
    • Browser caching via a fully configurable Cache-Control header
    • Server-side in-memory LRU cache (via lru-cache) for small files, togglable
  • ETag + 304 responses to avoid re-downloading unchanged files
  • Automatic streaming for files above a configurable size threshold, so large files are never fully buffered in memory
  • Three addressing modes:
    • "query" (default) — ?folder=icons&file=logo.svg, with fully renameable param names
    • "path" — a Next.js catch-all segment, e.g. /api/images/icons/logo.svg
    • "custom" — you supply your own function to extract folder/file from the request
  • Configurable MIME whitelist, headers, Content-Disposition, and error responses
  • Dual ESM + CJS build with full TypeScript types

Install

npm install @code-plate/next-image-serve

Peer dependencies: next >= 13.4, react >= 18

Basic usage (query mode, default)

// app/api/images/route.ts
import path from "path";
import { createImageHandler } from "@code-plate/next-image-serve";

export const { GET } = createImageHandler({
  basePath: path.join(process.cwd(), "assets/images"),
});
// any component
import Image from "next/image";

<Image
  src="/api/images?folder=products&file=sneaker-01.webp"
  alt="Running sneaker"
  width={400}
  height={400}
  unoptimized
/>;

Or build the URL with the type-safe helper:

import { buildImageUrl } from "@code-plate/next-image-serve";

const src = buildImageUrl({ base: "/api/images", folder: "products", file: "sneaker-01.webp" });
// => "/api/images?folder=products&file=sneaker-01.webp"

Renaming the query params

Every part of the query string is configurable — pick whatever names fit your API:

export const { GET } = createImageHandler({
  basePath: path.join(process.cwd(), "assets/images"),
  queryParams: { folder: "dir", file: "name" },
});
const src = buildImageUrl({
  base: "/api/images",
  folder: "products",
  file: "sneaker-01.webp",
  queryParams: { folder: "dir", file: "name" },
});
// => "/api/images?dir=products&name=sneaker-01.webp"

Path mode

For cleaner URLs like /api/images/products/sneaker-01.webp:

// app/api/images/[...segments]/route.ts
import { createImageHandler } from "@code-plate/next-image-serve";

export const { GET } = createImageHandler({ mode: "path" });
import { buildImageUrl } from "@code-plate/next-image-serve";

const src = buildImageUrl({
  base: "/api/images",
  folder: "products",
  file: "sneaker-01.webp",
  mode: "path",
});
// => "/api/images/products/sneaker-01.webp"

Custom mode

When neither query nor path addressing fits — e.g. you want to read the file id from a signed token, a header, or a completely different URL shape — take full control:

export const { GET } = createImageHandler({
  mode: "custom",
  basePath: path.join(process.cwd(), "assets/avatars"),
  resolveSegments: async (req) => {
    const userId = req.headers.get("x-user-id");
    return { folder: "avatars", file: `${userId}.webp` };
  },
});

All options

createImageHandler({
  // Absolute directory images are served from
  basePath: path.join(process.cwd(), "assets/images"), // default: "public/images"

  // How the request identifies the file
  mode: "query", // "query" | "path" | "custom"

  // Query param names (mode: "query" only) — name them anything you like
  queryParams: { folder: "folder", file: "file" },

  // Required when mode === "custom"
  resolveSegments: async (req, ctx) => ({ folder: "icons", file: "logo.svg" }),

  // Allowed MIME types
  allowedTypes: ["image/jpeg", "image/png", "image/webp", "image/svg+xml", "image/avif"],

  // Browser caching
  browserCacheMaxAge: 86400, // seconds
  immutable: true,
  // Full override, takes precedence over the two options above:
  cacheControl: (contentType) => "public, max-age=31536000, immutable",

  // Content-Disposition header, or `false` to omit it entirely
  contentDisposition: "inline", // "inline" | "attachment" | false

  // X-Content-Type-Options: nosniff toggle
  xContentTypeOptions: true,

  // Extra headers merged into every 200 response
  extraHeaders: (contentType) => ({ "X-Served-By": "next-image-serve" }),

  // Buffer + server-cache files at/below this size; stream everything larger
  streamThreshold: 512 * 1024, // bytes

  // Server-side LRU cache
  serverCache: {
    enabled: true,
    max: 200,
    maxSize: 50 * 1024 * 1024,
    ttl: 1000 * 60 * 60,
  },

  // Custom sanitizer applied to each path segment
  sanitize: (segment) => segment ?? "",

  // Custom error logger (defaults to console.error)
  onError: (error, ctx) => logger.error(error, ctx),

  // Per-case body/status overrides
  responses: {
    missingParam: { status: 400, body: { message: "file parameter is required" } },
    invalidPath: { status: 400 },
    notFound: { status: 404, body: { message: "not found" } },
    invalidType: { status: 400 },
    serverError: { status: 500 },
  },
});

Notes

  • The server-side LRU cache only holds files at or below streamThreshold; larger files always stream and are never cached in memory, to keep memory usage predictable.
  • The cache is per-instance/per-process, not shared across replicas. This package doesn't replace a distributed cache (e.g. Redis) — it only caches within a single running process.
  • basePath must be an absolute filesystem path on the server, not a public URL.
  • Because the image is served from an API route rather than the built-in Next.js image loader, you'll typically want unoptimized on next/image (as in the examples above), or you can wire this route up as a custom image loader.

Build

npm run build

Produces a dual ESM (dist/index.js) + CJS (dist/index.cjs) build with type declarations (dist/index.d.ts), via tsup.