- Notifications
You must be signed in to change notification settings - Fork11
Missing polyfill for multiple next.js middlewares
License
z4nr34l/nemo
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
A middleware composition library for Next.js applications that allows you to organize and chain middleware functions based on URL patterns.
npm install @rescale/nemo
pnpm add @rescale/nemo
bun add @rescale/nemo
- Path-based middleware routing
- Global middleware support (before/after)
- Context sharing between middleware via shared storage
- Support for Next.js native middleware patterns
- Request/Response header and cookie forwarding
- Middleware nesting and composition
- Built-in logging system accessible in all middleware functions
This example shows all possible options of NEMO usage and middlewares compositions, including nested routes:
import{createNEMO}from'@rescale/nemo';exportdefaultcreateNEMO({// Simple route with middleware chain'/api':[// First middleware in the chainasync(request,{ storage})=>{storage.set('timestamp',Date.now());// Continues to the next middleware},// Second middleware accesses shared storageasync(request,{ storage})=>{consttimestamp=storage.get('timestamp');console.log(`Request started at:${timestamp}`);}],// Nested routes using object notation'/dashboard':{// This middleware runs on /dashboardmiddleware:async(request)=>{console.log('Dashboard root');},// Nested route with parameter'/:teamId':{// This middleware runs on /dashboard/:teamIdmiddleware:async(request,{ params})=>{console.log(`Team dashboard:${params.teamId}`);},// Further nesting with additional parameter'/users/:userId':async(request,{ params})=>{console.log(`Team user:${params.teamId}, User:${params.userId}`);}},// Another nested route under /dashboard'/settings':async(request)=>{console.log('Dashboard settings');}},// Pattern matching multiple routes'/(auth|login)':async(request)=>{console.log('Auth page');}});
Each middleware in a chain is executed in sequence until one returns a response or all are completed. Nested routes allow you to organize your middleware hierarchically, matching more specific paths while maintaining a clean structure.
When a request matches a nested route, NEMO executes middleware in this order:
- Global
beforemiddleware (if defined) - Root path middleware (
/) for all non-root requests - Parent middleware (using the
middlewareproperty) - Child middleware
- Global
aftermiddleware (if defined)
If any middleware returns a response (like a redirect), the chain stops and that response is returned immediately.
typeNextMiddleware=(request:NextRequest,event:NemoEvent)=>NextMiddlewareResult|Promise<NextMiddlewareResult>;
The standard middleware function signature used in NEMO, compatible with Next.js native middleware.
typeMiddlewareConfig=Record<string,MiddlewareConfigValue>;
A configuration object that maps route patterns to middleware functions or arrays of middleware functions.
typeGlobalMiddlewareConfig=Partial<Record<"before"|"after",NextMiddleware|NextMiddleware[]>>;
Configuration for global middleware that runs before or after route-specific middleware.
functioncreateNEMO(middlewares:MiddlewareConfig,globalMiddleware?:GlobalMiddlewareConfig,config?:NemoConfig):NextMiddleware
Creates a composed middleware function with enhanced features:
- Executes middleware in order (global before → path-matched middleware → global after)
- Provides shared storage context between middleware functions
- Handles errors with custom error handlers
- Supports custom storage adapters
interfaceNemoConfig{debug?:boolean;silent?:boolean;errorHandler?:ErrorHandler;enableTiming?:boolean;storage?:StorageAdapter|(()=>StorageAdapter);}
To make it easier to understand, you can check the below examples:
Matches/dashboard route exactly.
/dashboardPath parameters allow you to capture parts of the URL path. The general pattern is:paramName whereparamName is the name of the parameter that will be available in the middleware function'sevent.params object.
Named parameters are defined by prefixing a colon to the parameter name (:paramName).
/dashboard/:teamThis matches/dashboard/team1 and providesteam param with valueteam1.
You can also place parameters in the middle of a path pattern:
/team/:teamId/dashboardThis matches/team/123/dashboard and providesteamId param with value123.
You can include multiple parameters in a single pattern:
/users/:userId/posts/:postIdThis matches/users/123/posts/456 and provides parametersuserId: "123", postId: "456".
Parameters can have a custom regexp pattern in parentheses, which overrides the default match:
/icon-:size(\\d+).pngThis matches/icon-123.png but not/icon-abc.png and providessize param with value123.
Parameters can be suffixed with a question mark (?) to make them optional:
/users/:userId?This matches both/users and/users/123.
Parameters can be wrapped in curly braces{} to create custom prefixes or suffixes:
/product{-:version}?This matches both/product and/product-v1 and providesversion param with valuev1 when present.
Parameters can be suffixed with an asterisk (*) to match zero or more segments:
/files/:path*This matches/files,/files/documents,/files/documents/work, etc.
Parameters can be suffixed with a plus sign (+) to match one or more segments:
/files/:path+This matches/files/documents,/files/documents/work, etc., but not/files.
You can match multiple pattern alternatives by using parentheses and the pipe character:
/(auth|login)This matches both/auth and/login.
The matcher fully supports Unicode characters in both patterns and paths:
/café/:itemThis matches/café/croissant and providesitem param with valuecroissant.
You can constrain route parameters to match only specific values or exclude certain values:
// Match only if :lang is either 'en' or 'cn'constnemo=newNEMO({"/:lang(en|cn)/settings":[// This middleware only runs for /en/settings or /cn/settings(req)=>{const{ lang}=req.params;// lang will be either 'en' or 'cn'returnNextResponse.next();},],});// Exclude specific values from matchingconstnemo=newNEMO({"/:path(!api)/:subpath":[// This middleware runs for any /:path/:subpath EXCEPT when path is 'api'// e.g., /docs/intro will match, but /api/users will not(req)=>{const{ path, subpath}=req.params;returnNextResponse.next();},],});
import{createNEMO}from'@rescale/nemo';exportconstmiddleware=createNEMO({// Simple route'/api':async(request)=>{// Handle API routes},// With parameter'/users/:userId':async(request,event)=>{// Access parameterconsole.log(`User ID:${event.params.userId}`);},// Optional pattern with custom prefix'/product{-:version}?':async(request,event)=>{// event.params.version will be undefined for '/product'// or the version value for '/product-v1'console.log(`Version:${event.params.version||'latest'}`);},// Pattern with custom matching'/files/:filename(.*\\.pdf)':async(request,event)=>{// Only matches PDF filesconsole.log(`Processing PDF:${event.params.filename}`);}});
import{createNEMO}from'@rescale/nemo';exportdefaultcreateNEMO({'/api{/*path}':apiMiddleware,},{before:[loggerMiddleware,authMiddleware],after:cleanupMiddleware,});
The Storage API allows you to share data between middleware executions:
import{createNEMO}from'@rescale/nemo';exportdefaultcreateNEMO({'/':[async(req,{ storage})=>{// Set valuesstorage.set('counter',1);storage.set('items',['a','b']);storage.set('user',{id:1,name:'John'});// Check if key existsif(storage.has('counter')){// Get values (with type safety)constcount=storage.get<number>('counter');constitems=storage.get<string[]>('items');constuser=storage.get<{id:number,name:string}>('user');// Delete a keystorage.delete('counter');}}]});
Access URL parameters through the event's params property:
import{createNEMO}from'@rescale/nemo';exportdefaultcreateNEMO({'/users/:userId':async(request,event)=>{const{ userId}=event.params;console.log(`Processing request for user:${userId}`);}});
NEMO provides built-in logging capabilities through the event object that maintains consistent formatting and respects the debug configuration:
import{createNEMO}from'@rescale/nemo';exportdefaultcreateNEMO({'/api':async(request,event)=>{// Debug logs (only shown when debug: true in config)event.log('Processing API request',request.nextUrl.pathname);try{// Your API logicconstresult=awaitprocessRequest(request);event.log('Request processed successfully',result);returnNextResponse.json(result);}catch(error){// Error logs (always shown)event.error('Failed to process request',error);// Warning logs (always shown)event.warn('This endpoint will be deprecated soon');returnNextResponse.json({error:'Internal Server Error'},{status:500});}}},undefined,{debug:true});
All logs maintain the "[NEMO]" prefix for consistency with internal framework logs.
- Middleware functions are executed in order until a Response is returned
- The storage is shared between all middleware functions in the chain
- Headers and cookies are automatically forwarded between middleware functions
- Supports Next.js native middleware pattern
I'm working with Next.js project for a few years now, after Vercel moved multiple/**/_middleware.ts files to a single/middleware.ts file, there was a unfilled gap - but just for now.After a 2023 retro I had found that there is no good solution for that problem, so I took matters into my own hands. I wanted to share that motivation with everyone here, as I think that we all need to remember how it all started.
Hope it will save you some time and would make your project DX better!
About
Missing polyfill for multiple next.js middlewares
Topics
Resources
License
Code of conduct
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Uh oh!
There was an error while loading.Please reload this page.
Contributors3
Uh oh!
There was an error while loading.Please reload this page.