@zanreal/nemo Documentation

repository·canary·Indexed 18 days ago

https://github.com/z4nr34l/nemo

A middleware composition library for Next.js applications that enables organized, path-based middleware routing and chaining. It supports advanced pattern matching with parameters, global before/after hooks, and a Storage API for sharing data between middleware. The package includes @zanreal/nemo-codemod to automate migration from the deprecated @rescale/nemo package.

Tokens
55.9K
Snippets
166
Records
193
Agent score
61%

What's inside @zanreal/nemo

  1. What is `storage` and how to use it for request-scoped data

    canary

    The storage object is a shared data repository accessible to all functions within a single request chain. It is designed to prevent redundant data fetching (e.g., fetching a user profile multiple times) by allowing you to fetch data once and store it for subsequent middleware or functions to consume.

    Key Characteristics:

    • Request Scoped: Each request receives its own instance of event, meaning storage content does not persist between different requests.
    • Default Implementation: By default, storage uses an in-memory implementation.
    • Performance Warning: If you implement a custom adapter that queries an external database, every request to that adapter will increase the Time to First Byte (TTFB). It is highly recommended to use key-value stores like Redis or Vercel Edge Config for custom adapters.
    import type { NextMiddleware } from "@zanreal/nemo";
    
    const example: NextMiddleware = async (req, { storage }) => {
      let user = undefined;
    
      if (!storage.has("user")) {
        user = await fetchUser();
        storage.set("user", user);
      } else {
        user = storage.get("user");
      }
    
      if (!user) {
        return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
      }
    };
  2. Execution order for nested routes

    canary

    When a request matches a nested route, the middleware chain is executed in the following order:

    1. Global before middleware (if defined).
    2. Root middleware (/) — for all requests except the root itself.
    3. Parent middleware.
    4. Child middleware.
    5. Global after middleware (if defined).

    Important: Returning a response (such as a redirect or a direct response) at any stage breaks the chain. The response is sent immediately to the browser, and any subsequent middleware in the chain (including child routes) will not execute.

  3. Use nested routes in the middleware map

    canary

    Instead of a function, a pattern can be assigned an object. The middleware key defines the logic for that specific route, while other keys define nested sub-routes.

    Execution Order: Middleware for a parent route always executes before the middleware of any matching sub-route.

    Parametric segments (e.g., /:id) work at any nesting level, and extracted segments are available in event.params.

    import { createNEMO } from "@zanreal/nemo";
    
    const middleware = createNEMO({
      "/admin": {
        // Middleware for /admin route
        middleware: async (request) => {
          console.log("Admin section accessed");
        },
    
        // Nested routes under /admin
        "/users": async (request) => {
          console.log("Admin users section");
        },
        "/settings": async (request) => {
          console.log("Admin settings section");
        },
      },
    });
  4. Important caveats for @zanreal/nemo-codemod

    canary

    While the codemod is an AST-based transform (not a simple text search-and-replace), users should be aware of the following:

    • Shadowed require: The transform matches require(...) and require.resolve(...) by shape. If you have a local function named require that is not the standard Node.js/bundler require, any call to it using the string '@rescale/nemo' will be incorrectly rewritten.
    • Non-code content: Comments, docstrings, and changelogs containing @rescale/nemo are not changed. This is intentional to preserve historical context.
    • Lockfiles: The tool does not modify lockfiles. You must run your package manager's install command (e.g., npm install) after running the codemod.
    • Safety: If the tool encounters a file it cannot transform, it will exit with a non-zero status and stop before modifying package.json to prevent leaving your project in a broken state where sources use the new name but dependencies use the old one.
  5. How route matchers work in v2

    canary
    In the v2 line of Nemo, route matching is powered by path-to-regexp. This allows you to define patterns for middleware routes, including named parameters and wildcards, similar to how the matcher prop works in Next.js configuration. When a route matches, any parsed parameters are passed into your middleware functions.
  6. Configure global middlewares

    canary

    The globalMiddlewares object allows you to define logic that runs globally across all routes. It is a Record<"before" | "after", MiddlewareFunction | MiddlewareFunction[]>.

    • before: Executes before any route-specific middleware in the middlewares map.
    • after: Executes after any route-specific middleware.

    Like route-specific middlewares, these can be a single function or an array of functions for chaining.

    const globalMiddlewares = {
      before: [
        async ({ request, context }: MiddlewareFunctionProps) => { /* runs first */ },
        async ({ request, context }: MiddlewareFunctionProps) => { /* runs second */ }
      ],
      after: async ({ request }: MiddlewareFunctionProps) => {
        // runs after route middleware
      }
    };
  7. Route Matching Rules for Nested vs Top-level Routes

    canary

    NEMO applies different matching logic depending on whether a route is a standalone top-level route or a parent in a hierarchy:

    • Top-level routes (no nesting): Match the exact path only. For example, "/foo": fooMiddleware will match /foo but will not match /foo/bar.
    • Parent routes (with nesting): Match both the exact path and any child paths. For example, a route defined with "/parent": { middleware: ..., "/child": ... } will match both /parent and /parent/child.
  8. Configure route-specific middlewares

    canary

    The middlewares object is a Record<string, MiddlewareFunction | MiddlewareFunction[]> where keys are route paths and values are the middleware to execute.

    Matchers

    Route paths use path-to-regexp syntax, similar to the Next.js matcher config. This allows for simple paths or complex route segments with parameters.

    Middleware Execution

    For each route, you can provide either a single MiddlewareFunction or an array of functions (MiddlewareFunction[]). If an array is provided, the functions are executed in order.

    Supported Matcher Examples:

    • Simple path: '/api'
    • Route segments: '/team/:slug{/*path}' (matches /team/abc/def)
    const middlewares = {
      // Single function
      '/api': async ({ request }: MiddlewareFunctionProps) => {
        // logic
      },
    
      // Chained functions executed in order
      '/api/v2': [
        async ({ request, context }: MiddlewareFunctionProps) => { /* first */ },
        async ({ request, context }: MiddlewareFunctionProps) => { /* second */ }
      ],
    
      // Complex route segment
      '/team/:slug{/*path}': async ({ request }) => { /* logic */ }
    };
  9. Use Global Middlewares with GlobalMiddlewareConfig

    canary

    Global middlewares (type GlobalMiddlewareConfig) execute for every request, regardless of the route. They are divided into two lifecycle stages:

    • before: Executes before any route-specific middleware. Can be a single function or an array of functions.
    • after: Executes after all route-specific middleware have completed. Can be a single function or an array of functions.
    import { type GlobalMiddlewareConfig } from "@zanreal/nemo";
    
    const globalMiddlewares = {
      before: async (request, event) => {
        console.log("Before any route middleware");
      },
      after: async (request, event) => {
        console.log("After all route middleware");
      },
    } satisfies GlobalMiddlewareConfig;
  10. Use cross-middleware caching with built-in storage

    canary

    To reduce heavy operations like database queries within a single request chain, use the built-in storage object provided to the NextMiddleware function. This allows you to cache data in one middleware and retrieve it in subsequent middlewares in the same chain.

    import { NextMiddleware } from '@zanreal/nemo';
    
    export const auth: NextMiddleware = (request, { storage }) => {
      const [user, roles] = await Promise.all([
        fetchUser(),
        fetchRoles(),
      ]);
    
      storage.set('user', user);
      storage.set('roles', roles);
    
      if(!user || !roles) {
        return NextResponse.redirect('/login');
      }
    }
  11. Conditionally skip middlewares using event.skip()

    canary

    Use event.skip() to stop the execution of the remaining middlewares in the current chain section without returning a terminating response. This is useful for performance optimizations like cache hits.

    • event.skip(): Skips remaining middlewares but allows the after chain to run (useful for cleanup).
    • event.skip({ skipAfter: true }): Skips both the remaining middlewares AND the after chain (useful when no cleanup is required).
    import type { NextMiddleware } from '@zanreal/nemo';
    import { NextResponse } from 'next/server';
    
    const cacheCheck: NextMiddleware = async (request, { storage, event }) => {
      const cacheKey = request.nextUrl.pathname;
      const cached = storage.get(cacheKey);
      
      if (cached) {
        // Skip remaining middlewares, but allow after chain to run
        event.skip();
        return NextResponse.next({
          headers: { 'x-cached': 'true' }
        });
      }
    };
    
    const earlyExit: NextMiddleware = async (request, { event }) => {
      if (request.headers.get('x-skip-all') === 'true') {
        // Skip remaining middlewares AND the after chain
        event.skip({ skipAfter: true });
        return NextResponse.next();
      }
    };
  12. How nested routes work in NEMO

    canary

    NEMO allows you to organize middleware into a hierarchy that mirrors your application's route structure. This is achieved by defining a route key that contains an object with a middleware property and additional nested route keys.

    • Parent Middleware: Runs for the parent route and every matching sub-route.
    • Child Middleware: Runs only for the specific sub-route it is defined under.
    • Execution: When a nested route is hit, both the parent and child middleware execute sequentially.
    import { createNEMO } from "@zanreal/nemo";
    
    const middleware = createNEMO({
      "/admin": {
        // Middleware for /admin route
        middleware: async (req) => {
          req.headers.set("x-section", "admin");
          return NextResponse.next();
        },
    
        // Nested route for /admin/users
        "/users": async (req) => {
          // This middleware only runs for /admin/users
          return NextResponse.next({
            headers: { "x-admin-users": "true" },
          });
        },
      },
    });