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

@tinystack/next-middleware

v0.2.0

Published

Composable, type-safe middleware chains for Next.js route handlers — middleware ordering enforced at compile time.

Downloads

247

Readme

@tinystack/next-middleware

Composable, type-safe middleware chains for Next.js route handlers — with middleware ordering enforced at compile time.

// app/api/projects/route.ts
import {
  createMiddlewareChain,
  withQuery,
} from "@tinystack/next-middleware"
import { z } from "zod"

export const GET = createMiddlewareChain()
  .use(withAuth) // adds ctx.user
  .use(withQuery(z.object({ page: z.coerce.number().default(1) })))
  .handle(async (req, ctx) => {
    //                 ^ ctx is fully typed: { user: User; query: { page: number } }
    return Response.json(await listProjects(ctx.user, ctx.query.page))
  })

Why

Every Next.js codebase grows a hand-rolled use(...) chain eventually. Most of them get two things wrong:

  1. Ordering is unchecked. A middleware that reads ctx.user can run before the one that sets it — and nothing complains until production.
  2. Responses get lost. A middleware that forgets return next() silently swallows the handler's response.

This library fixes both:

  • use() rejects, at compile time, any middleware whose requirements the chain doesn't yet provide:

    const needsUser = createTypedMiddleware<{ org: string }, { user: string }>(
      async (req, ctx, next) => next({ org: await orgOf(ctx.user) })
    )
    
    createMiddlewareChain().use(needsUser)
    //                          ^^^^^^^^^ ✖ Type error: chain provides {},
    //                                      middleware requires { user: string }
    
    createMiddlewareChain().use(withUser).use(needsUser) // ✔ compiles
  • The runtime guarantees exactly one response per request: a dropped response (await next() without return) is rescued and reported via onDroppedResponse; a chain that produces nothing returns a JSON 500 (or your fallbackResponse); calling next() twice throws.

Install

pnpm add @tinystack/next-middleware

next (≥14) and zod (≥3.25, if you use the validation middlewares) are peer dependencies.

Writing middleware

A middleware declares what it adds to ctx and what it requires from middlewares before it:

import { createTypedMiddleware } from "@tinystack/next-middleware"

//                                     Adds ─────────┐   Requires ──┐
const withOrg = createTypedMiddleware<{ org: Org }, { user: User }>(
  async (req, ctx, next) => {
    const org = await findOrg(ctx.user)
    if (!org) return Response.json({ error: "No org" }, { status: 403 })

    return next({ org }) // always *return* next()
  }
)

Short-circuit by returning a Response without calling next(). Anything a middleware throws propagates out of the chain — wrap it with withErrorBoundary if you want throws converted to responses.

Included middlewares

The validation middlewares parse one request surface with a zod schema, add the typed result to ctx, and respond with a JSON 400 (including the zod issues) on invalid input. Override that via onInvalid — return your own Response or throw into your error boundary.

| Middleware | Adds | Notes | | ---------------------------- | ------------- | ----------------------------------------------------------------------------------- | | withQuery(schema, opts?) | ctx.query | Empty values become undefined; repeated keys become arrays | | withBody(schema, opts?) | ctx.body | Malformed JSON fails validation like any other bad input; consumes the request body | | withParams(schema, opts?) | ctx.params | Awaits the Next.js 15+ params promise before validating | | withHeaders(schema, opts?) | ctx.headers | Header names are lowercased; write schema keys in lowercase |

export const POST = createMiddlewareChain()
  .use(withParams(z.object({ projectId: z.uuid() })))
  .use(withBody(z.object({ name: z.string().min(1) })))
  .handle(async (req, ctx) =>
    Response.json(await rename(ctx.params.projectId, ctx.body.name))
  )

Error boundary

withErrorBoundary(onError) catches anything thrown by later middlewares or the handler and converts it to a response. Add it as the first use() so it wraps the whole chain; the error-to-response mapping is entirely yours:

export const GET = createMiddlewareChain()
  .use(
    withErrorBoundary((error, req) => {
      logger.error("unhandled", { error, path: req.nextUrl.pathname })
      return Response.json({ error: "Internal error" }, { status: 500 })
    })
  )
  .use(withAuth)
  .handle(async (req, ctx) => Response.json(await riskyThing(ctx.user)))

It adds nothing to ctx, so it composes anywhere in the chain — but it only sees throws from middlewares after it.

Chain options

createMiddlewareChain({
  // Log dropped responses with your logger (default: console.warn)
  onDroppedResponse: ({ method, pathname }) =>
    logger.error("dropped", { method, pathname }),
  // Response when nothing in the chain produced one (default: JSON 500)
  fallbackResponse: (req) =>
    Response.json({ error: "No response" }, { status: 500 }),
})

Chains are immutable — use() returns a new chain — so you can fork a shared base chain safely:

const authed = createMiddlewareChain().use(withAuth)

export const GET = authed.use(withQuery(listSchema)).handle(listProjects)
export const POST = authed.use(withBody(createSchema)).handle(createProject)

When not to use it

Boring JSON APIs are the sweet spot. Streaming responses, webhooks with raw body signatures, and file uploads are often clearer as plain route handlers.

License

MIT