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

next-fetch-panel

v0.6.0

Published

Real-time server-side fetch inspector panel for Next.js App Router

Readme

next-fetch-panel

A real-time DevTools panel that intercepts all server-side fetch() calls in a Next.js App Router application and streams them to a floating panel in the browser. Each browser session only sees its own requests, so it is safe to run in shared environments like staging.


Requirements

  • Next.js 15 or 16 (App Router)
  • React 19
  • Node.js runtime (not Edge)

Installation

npm install next-fetch-panel

Wiring it up

There are four integration points. Each one has a simple form (one line, nothing else needed) and a composable form (for projects that already have logic in those files).


1. instrumentation.ts — fetch interception

Simple — nothing else in your instrumentation.ts:

// instrumentation.ts
export { register } from "next-fetch-panel/patch";

Composable — you already have setup code:

// instrumentation.ts
import { registerDevPanel } from "next-fetch-panel/patch";
import { registerOTel } from "@vercel/otel";

export async function register() {
  registerOTel("my-app");
  await registerDevPanel();
}

registerDevPanel skips itself when NEXT_RUNTIME !== "nodejs" so it never runs in Edge contexts.


2. middleware.ts / proxy.ts — session isolation

Next.js 16+ renamed this file from middleware.ts to proxy.ts and the export from middleware to proxy. Both versions work the same way — match the file name and export name to your Next.js version.

Simple — no other middleware (Next.js ≤ 15):

// middleware.ts
export { middleware, config } from "next-fetch-panel/middleware";

Simple — Next.js 16+:

// proxy.ts
import { withDevPanel } from "next-fetch-panel/middleware";

export const proxy = withDevPanel();
export const config = { matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"] };

Composable — you already have your own logic:

// proxy.ts (Next.js 16+) or middleware.ts (Next.js ≤ 15)
import { withDevPanel } from "next-fetch-panel/middleware";

export const proxy = withDevPanel(async (request) => {
  if (!request.cookies.has("auth-token"))
    return NextResponse.redirect(new URL("/login", request.url));
  // return nothing to let the request continue normally
});

export const config = {
  matcher: ["/(ca|us)/:path*", "/((?!api|_next|.*\\..*).*)"],
};

withDevPanel handles NextResponse.next() internally — only return a response when you want to block or redirect. The function itself is the middleware handler, so you can export it directly as proxy, middleware, or any name your project uses.


3. app/api/dev-network/route.ts — SSE stream

Simple — no access control needed:

// app/api/dev-network/route.ts
export { dynamic, GET } from "next-fetch-panel/route";

Composable — restrict access (recommended for staging):

// app/api/dev-network/route.ts
import { createDevPanelRoute } from "next-fetch-panel/route";

export const dynamic = "force-dynamic";
export const GET = createDevPanelRoute({
  guard: (request) => {
    const token = request.headers.get("authorization");
    if (!isValidAdminToken(token))
      return new Response("Forbidden", { status: 403 });
  },
});

4. app/layout.tsx — panel component

// app/layout.tsx
import { DevNetworkPanel } from "next-fetch-panel";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        {children}
        <DevNetworkPanel />
      </body>
    </html>
  );
}

Both modes are resizable. In sheet mode, drag the handle at the inward-facing edge to change its height (bottom/top) or width (left/right). In fixed modal mode, all four edges have handles — drag any edge to resize. The size is saved to localStorage automatically.


Controlling the panel programmatically

Use the useDevNetworkPanel hook to open, close, or toggle the panel from your own components — without relying on the default floating button.

"use client";
import { useDevNetworkPanel } from "next-fetch-panel";

export function MyToolbar() {
  const { isOpen, toggle } = useDevNetworkPanel();
  return (
    <button onClick={toggle}>
      {isOpen ? "Hide" : "Show"} network panel
    </button>
  );
}

The hook returns { isOpen, open, close, toggle }. All four values are stable references — safe to use as event handlers without useCallback.

To hide the default floating button (when you're providing your own trigger), pass showToggleButton={false} to <DevNetworkPanel>:

<DevNetworkPanel showToggleButton={false} />

Toggle button position

Use buttonPosition to place the default toggle button in any of six positions (default: "bottom-right"):

<DevNetworkPanel buttonPosition="bottom-center" />

| Value | Position | |-------|----------| | "bottom-right" | Bottom-right corner (default) | | "bottom-center" | Bottom edge, centered | | "bottom-left" | Bottom-left corner | | "top-right" | Top-right corner | | "top-center" | Top edge, centered | | "top-left" | Top-left corner |

In fixed panel mode, the panel container anchors to the same corner/edge as the button.


Redacting secrets

By default, the panel redacts:

  • URL params: api_key, apikey, secret, token, access_token, password, key
  • Headers: authorization, x-api-key, x-secret, x-auth-token
  • JSON body keys: password, secret, token, api_key, apiKey, accessToken, access_token

Pass a redact option to registerDevPanel and/or createDevPanelAxiosInterceptors to customize:

// extend defaults — keep existing patterns, add your own
await registerDevPanel({
  redact: (defaults) => ({
    ...defaults,
    urlParams: [...defaults.urlParams, "shop_token"],
    bodyKeys:  [...defaults.bodyKeys,  "refreshToken"],
  }),
});

// override a category entirely — only these values will be redacted
await registerDevPanel({
  redact: { headers: ["x-my-secret"] },
});

// disable a category — show everything
await registerDevPanel({
  redact: { headers: [] },
});

The same redact option is available on createDevPanelAxiosInterceptors({ redact: ... }).

The actual HTTP request is never modified — only the copy kept for display.


Capture limits

By default, request and response bodies are capped at 50 KB and the server keeps the last 100 entries in memory. Both limits are configurable:

// instrumentation.ts
await registerDevPanel({
  bodyLimit:  200_000,  // bytes — applies to request and response bodies (default: 50_000)
  bufferSize: 200,      // entries kept in the server-side ring buffer (default: 100)
});

// axios client
createDevPanelAxiosInterceptors({
  bodyLimit: 200_000,   // same option, independent from the fetch patch
});

// app/layout.tsx — keep in sync with bufferSize above
<DevNetworkPanel bufferSize={200} />

bufferSize on <DevNetworkPanel> controls how many entries the panel displays. Keep it in sync with the bufferSize passed to registerDevPanel so the server buffer and the client display cap match.


Using axios

Register the dev panel interceptors before your own interceptors. Use a synchronous require() so they run first in the response chain:

// your-axios-client.ts
import axios from "axios";

const http = axios.create({ baseURL: "https://api.example.com" });

if (typeof window === "undefined") {
  try {
    const { createDevPanelAxiosInterceptors } = require("next-fetch-panel/interceptor");
    const { request, response, error } = createDevPanelAxiosInterceptors();
    http.interceptors.request.use(request);
    http.interceptors.response.use(response, error);
  } catch {}
}

export default http;

No axios configuration change is required — adapter: 'fetch' is not needed. The interceptors work with any axios adapter.

If your project makes server-side requests only through axios (no native fetch() in Server Components or Route Handlers), you can skip instrumentation.ts entirely — the interceptors are sufficient. If you use both, set up instrumentation.ts as well so native fetch() calls are also captured.


Disabling per environment

Every entry point accepts an enabled option (or prop) that defaults to true. Pass false to make that layer a complete no-op — no patching, no cookies, no SSE stream, no panel rendered.

// instrumentation.ts
await registerDevPanel({ enabled: process.env.NODE_ENV !== "production" });

// middleware.ts / proxy.ts
export const middleware = withDevPanel(handler, { enabled: process.env.NODE_ENV !== "production" });

// app/api/dev-network/route.ts
export const GET = createDevPanelRoute({ enabled: process.env.NODE_ENV !== "production" });

// axios client
createDevPanelAxiosInterceptors({ enabled: process.env.NODE_ENV !== "production" });

// app/layout.tsx
<DevNetworkPanel enabled={process.env.NODE_ENV !== "production"} />

You can use any condition — NODE_ENV, a custom env var, a feature flag, or any boolean. Each entry point is independent, so you can disable just the fetch patch while keeping the panel open for other sessions, or shut everything off at once.


Security notes

| Concern | How it is handled | |---|---| | User A seeing User B's requests | Each browser session gets a unique __dev_sid cookie. The SSE stream filters strictly by session ID. | | Panel accessible to anyone | Use the guard option on createDevPanelRoute. | | Secrets leaking to the browser | Configured patterns are replaced with [REDACTED] before entries reach the store. | | Running in production | Pass enabled: process.env.NODE_ENV !== "production" to each entry point. |


Exporting logs for test automation

The snapshot endpoint returns the current server-side log buffer for a session as JSON. Call it in test teardown to save network logs as CI artifacts.

1. Create the route:

// app/api/dev-network/snapshot/route.ts
export { dynamic, GET_SNAPSHOT as GET } from "next-fetch-panel/route";

With access control:

// app/api/dev-network/snapshot/route.ts
import { createDevPanelSnapshot } from "next-fetch-panel/route";

export const dynamic = "force-dynamic";
export const GET = createDevPanelSnapshot({
  guard: (request) => {
    if (!isValidToken(request.headers.get("authorization")))
      return new Response("Forbidden", { status: 403 });
  },
});

2. Call it from your test runner:

// Playwright example — save logs as artifact after each test
test.afterEach(async ({ request, page }, testInfo) => {
  const cookie = (await page.context().cookies()).find(c => c.name === "__dev_sid");
  if (!cookie) return;

  const res = await request.get("/api/dev-network/snapshot", {
    headers: { Cookie: `__dev_sid=${cookie.value}` },
  });
  const { entries } = await res.json();

  await testInfo.attach("network-logs.json", {
    contentType: "application/json",
    body: JSON.stringify(entries, null, 2),
  });
});

Response shape:

{ entries: LogEntry[] }

The snapshot only includes entries captured since the session was created, up to the configured bufferSize (default: 100). Increase it in CI if your test suites make many requests:

await registerDevPanel({ bufferSize: 2000 });

Public API

import { DevNetworkPanel, useDevNetworkPanel } from "next-fetch-panel";
import type { LogEntry, ButtonPosition } from "next-fetch-panel";

import { registerDevPanel } from "next-fetch-panel/patch";
import { withDevPanel } from "next-fetch-panel/middleware";
import { createDevPanelRoute, createDevPanelSnapshot } from "next-fetch-panel/route";
import { createDevPanelAxiosInterceptors } from "next-fetch-panel/interceptor";