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

@flefebvre/next-pipe

v1.0.0

Published

A typed, onion-model middleware system for Next.js route handlers, server actions, form actions, and pages.

Readme

next-pipe

npm license

A typed, onion-model middleware system for Next.js route handlers, server actions, form actions, and pages — plus a generated, fully typed client for your routes.

Requires Next.js ≥ 16 (App Router), React ≥ 19, TypeScript ≥ 5, Node ≥ 22. zod v4 is an optional peer dependency, needed only by the validation middlewares.

Follows semver: breaking changes only land in major versions — see Migrating when upgrading across one.

Why

The Next.js App Router gives you a handful of server entry points — route handlers, server actions, form actions, pages, layouts, and templates — and no middleware system for any of them. So every file re-implements the same prologue by hand: check the session, parse and validate the input, load the entity or 404, shape the error response. next-pipe replaces that with one onion-model middleware system that works identically across all of these surfaces, on top of the native Next.js primitives (your route.ts stays a plain HTTP endpoint, your actions stay server actions). Because middlewares are typed end to end — including their short-circuit responses — a generated client can read back the exact response union of every route.

export const POST = routePipe<RouteContext<"/api/notes/[id]/like">>()
  .use(AuthMiddleware) //                                            → 401
  .use(BodyValidationMiddleware, z.object({ like: z.boolean() })) // → 400
  .handle(async ({ ctx, input, user }) => {
    const { id } = await ctx.params;
    await db.setLike(id, user.username, input.like);
    return json(200, { liked: input.like });
  });

The 401 and 400 paths you didn't write are still there — handled by the middlewares, turned into real Responses, and included in this route's response type, which the generated client gets for free.

How is this different from…

  • next-safe-action / zsa — excellent action builders, but they cover server actions only. next-pipe applies the same middleware model to route handlers, form actions, and pages too, and adds the generated typed client for routes.
  • tRPC — replaces your HTTP surface with an RPC router that the client must import. next-pipe keeps native route.ts endpoints (callable by anything — webhooks, mobile apps, curl); client types come from codegen over import type, so no server code approaches the client bundle.
  • ts-rest / Hono — contract-first or bring-your-own-framework: you describe the API separately, then implement it. next-pipe goes the other way and derives the contract from the handlers you already wrote.

Install

pnpm add @flefebvre/next-pipe
# optional, for the validation middlewares and actionPipe(schema)/formActionPipe(schema);
# the schemaless actionPipe()/formActionPipe() work without it:
pnpm add zod

Quickstart: a route, its client

1. Write a middleware

A middleware is a class with a before step (and optionally an after step). before either merges typed values into the handler's input with next(...), or short-circuits with interrupt(...):

// lib/middlewares/auth-middleware.ts
import { next, interrupt } from "@flefebvre/next-pipe/server";
import { BeforeMiddleware } from "@flefebvre/next-pipe/middlewares";
import { getSessionUser } from "@/lib/auth"; // your own session logic

export class AuthMiddleware extends BeforeMiddleware {
  async before() {
    const user = await getSessionUser();
    if (!user) {
      // Short-circuits the handler. The interrupt travels back through the
      // `after` chain, becomes a real 401 Response, and joins the route's
      // response union.
      return interrupt({ status: 401, json: { error: "Not signed in" } } as const);
    }
    // Merged into the handler's input, fully typed.
    return next({ user });
  }
}

2. Build the route

routePipe() wires in ResponseMiddleware for you: handlers return plain { status, json } objects (the json() helper keeps them precisely typed) and they come out as real Responses:

// app/api/notes/[id]/like/route.ts
import z from "zod";
import { routePipe } from "@flefebvre/next-pipe/pipes";
import { json } from "@flefebvre/next-pipe/server";
import { BodyValidationMiddleware } from "@flefebvre/next-pipe/middlewares/routes";
import { AuthMiddleware } from "@/lib/middlewares/auth-middleware";
import { db } from "@/lib/db";

export const POST = routePipe<RouteContext<"/api/notes/[id]/like">>()
  .use(AuthMiddleware)
  .use(BodyValidationMiddleware, z.object({ like: z.boolean() }))
  .handle(async ({ ctx, input, user }) => {
    // input is { like: boolean }, user comes from AuthMiddleware — all typed.
    const { id } = await ctx.params;
    await db.setLike(id, user.username, input.like);
    return json(200, { liked: input.like });
  });

3. Generate the client

$ npx next-pipe gen
✓ api/notes/[id]/like → src/generated/routes/api/notes/[id]/like.ts (POST)
✓ barrel → src/generated/routes/index.ts

The generator walks your app/ directory with the TypeScript type checker and emits one tiny module per route plus a routes barrel. Generated files use import type only, so no server code can leak into the client bundle. Tip: add "gen": "next-pipe gen" to your package.json scripts and re-run it when routes change.

4. Call it — with the exact response union

import { callRoute } from "@flefebvre/next-pipe/client";
import { routes } from "@/generated/routes";

const res = await callRoute(routes.api.notes(id).like.POST(), { json: { like: true } });
// res: { status: 200; json: { liked: boolean } }
//    | { status: 401; json: { error: string } }
//    | { status: 400; json: … }  ← schema failure, with typed field errors

if (res.status === 200) {
  res.json.liked; // boolean
}

callRoute requires json (and query) exactly when the route declares them, and returns the union of everything the route can answer — handler returns and middleware interrupts. In client components, useApiCall(builder) wraps this with useTransition pending state — see Client & hooks.

Package map

| Import | Exports | Docs | | --- | --- | --- | | @flefebvre/next-pipe/pipes | routePipe, actionPipe, formActionPipe, pagePipe, layoutPipe, templatePipe | route · action · form · page · layout · template | | @flefebvre/next-pipe/server | Pipe, entry, createPipe, next, interrupt, success, error, json, merge | Core concepts | | @flefebvre/next-pipe/client | callRoute, defineRoute, useApiCall, getActionError, getActionInput | Client & hooks | | @flefebvre/next-pipe/middlewares | Middleware, BeforeMiddleware, AfterMiddleware, OutputTypeMiddleware + authoring types | Custom middlewares | | @flefebvre/next-pipe/middlewares/routes | ResponseMiddleware, BodyValidationMiddleware, QuerystringMiddleware | Built-in middlewares | | @flefebvre/next-pipe/middlewares/actions | InputValidationMiddleware | Built-in middlewares | | @flefebvre/next-pipe/middlewares/form-actions | FormValidationMiddleware | Built-in middlewares | | @flefebvre/next-pipe/middlewares/pages | SearchParamsMiddleware | Built-in middlewares | | next-pipe (bin) | next-pipe gen | Codegen |

Going further

  • Core concepts — the onion model: before/after phases, next vs interrupt, how context merges across middlewares, and why interrupts flow back through the afters of earlier middlewares.
  • routePipe — route handlers: response unions, RouteContext params, status/json typing.
  • actionPipe — typed server actions: zod input validation, the success/error result shape, reading errors with getActionError.
  • formActionPipe<form> actions: FormData parsing, schema validation, typed field errors back to the form.
  • pagePipe — run the same middlewares in front of a page or server component (e.g. redirect guests to /login).
  • layoutPipe / templatePipe — gate a whole segment from its layout or template; typed children, params, and parallel-route slots via LayoutProps.
  • Built-in middlewares — body, querystring, input and form validation; response shaping; output typing.
  • Write your own middlewareBeforeMiddleware, AfterMiddleware, parametrized constructors (.use(M, ...args)), interrupts, and the config type channel that feeds the generated client.
  • Codegen — how next-pipe gen analyzes routes through the TypeScript checker, every CLI flag, and CI setup.
  • Migrating — version-to-version upgrade notes; only needed when bumping across a breaking change.
  • Client & hooksdefineRoute, callRoute semantics (declared statuses return, everything else throws), and the useApiCall hook.

Agent skill

If you use an AI coding agent (Claude Code, Cursor, Codex, …), this repo ships an installable skill that teaches the agent next-pipe's pipes, middlewares, codegen, and client:

npx skills add flolefebvre/next-pipe

(Uses the skills CLI; or just copy skills/next-pipe/ into your agent's skills directory, e.g. .claude/skills/.)

License

MIT