Overview
EverShop’s middleware system is a delegated, dependency-based architecture that allows fine-grained control over request processing. Middleware functions are automatically discovered, sorted by dependencies, and executed in a chain.
Middleware Basics
Middleware Function Signature
Middleware functions come in two types:
Normal Middleware
export default function middleware ( request , response , next ) {
// Process request
// Modify request or response
next (); // Call next middleware
}
Error Middleware
export default function errorHandler ( error , request , response , next ) {
// Handle error
console . error ( error );
response . status ( 500 ). json ({ error: error . message });
}
Middleware with 4 parameters is automatically recognized as an error handler and only called when an error occurs.
File-Based Middleware Discovery
Middleware files are automatically discovered from route directories:
packages/evershop/src/lib/middleware/scanForMiddlewareFunctions.js
export function scanForMiddlewareFunctions ( path ) {
let middlewares = [];
readdirSync ( resolve ( path ), { withFileTypes: true })
. filter (
( dirent ) =>
dirent . isFile () &&
/ \. js $ / . test ( dirent . name ) &&
! / ^ [ A-Z ] / . test ( dirent . name [ 0 ]) // Skip React components
)
. forEach (( dirent ) => {
const middlewareFunc = resolve ( path , dirent . name );
middlewares = middlewares . concat ( parseFromFile ( middlewareFunc ));
});
return middlewares ;
}
Files starting with uppercase letters are skipped (these are React components). Only lowercase .js files are treated as middleware.
Dependency-Based Ordering
Naming Convention
Middleware execution order is controlled through file naming :
Pattern Example Meaning name.jsvalidate.jsBasic middleware [after]name.js[auth]validate.jsRuns after auth name[before].jsauth[context].jsRuns before context [after]name[before].js[auth]validate[handler].jsRuns after auth, before handler
Multiple Dependencies
Use commas to specify multiple dependencies:
[auth,validate,sanitize]handler.js # Runs after auth, validate, AND sanitize
auth[context,session].js # Runs before context AND session
Parsing Logic
packages/evershop/src/lib/middleware/parseFromFile.js
export function parseFromFile ( path ) {
const name = basename ( path );
let m = {};
let id ;
// Pattern: [after1,after2]id.js
if ( / ^ ( \[ ) [ a-zA-Z1-9., ] + ( \] ) [ a-zA-Z1-9 ] + . js $ / . test ( name )) {
const split = name . split ( / [ \[\] ] + / );
id = split [ 2 ]. substr ( 0 , split [ 2 ]. indexOf ( '.' )). trim ();
m = {
id ,
middleware: buildMiddlewareFunction ( id , path ),
after: split [ 1 ]. split ( ',' ). filter (( a ) => a . trim () !== '' ),
path
};
}
// Pattern: id[before1,before2].js
else if ( / ^ [ a-zA-Z1-9 ] + ( \[ ) [ a-zA-Z1-9, ] + ( \] ) . js $ / . test ( name )) {
const split = name . split ( / [ \[\] ] + / );
id = split [ 0 ]. trim ();
m = {
id ,
middleware: buildMiddlewareFunction ( id , path ),
before: split [ 1 ]. split ( ',' ). filter (( a ) => a . trim () !== '' ),
path
};
}
// Pattern: [after]id[before].js
else if (
/ ^ ( \[ ) [ a-zA-Z1-9, ] + ( \] ) [ a-zA-Z1-9 ] + ( \[ ) [ a-zA-Z1-9, ] + ( \] ) . js $ / . test ( name )
) {
const split = name . split ( / [ \[\] ] + / );
id = split [ 2 ]. trim ();
m = {
id ,
middleware: buildMiddlewareFunction ( id , path ),
after: split [ 1 ]. split ( ',' ). filter (( a ) => a . trim () !== '' ),
before: split [ 3 ]. split ( ',' ). filter (( a ) => a . trim () !== '' ),
path
};
}
// Simple: id.js
else {
const split = name . split ( '.' );
id = split [ 0 ]. trim ();
m = {
id ,
middleware: buildMiddlewareFunction ( id , path ),
path
};
}
// Auto-add default dependencies for API routes
const route = getRouteFromPath ( path );
if ( route . region === 'api' ) {
if ( m . id !== 'context' && m . id !== 'apiErrorHandler' ) {
m . before = ! m . before ? [ 'apiResponse' ] : m . before ;
m . after = ! m . after ? [ 'escapeHtml' , 'auth' ] : m . after ;
}
} else if ( m . id !== 'context' && m . id !== 'errorHandler' ) {
m . before = ! m . before ? [ 'notFound' ] : m . before ;
m . after = ! m . after ? [ 'auth' ] : m . after ;
}
return [{ ... m , ... route }];
}
API routes automatically get after: ['escapeHtml', 'auth'] and before: ['apiResponse'] unless explicitly set or named context or apiErrorHandler.
Topological Sorting
Middlewares are sorted using topological sorting based on dependencies:
packages/evershop/src/lib/middleware/sort.js
import Topo from '@hapi/topo' ;
export function sortMiddlewares ( middlewares = []) {
// Filter out middlewares with unresolved dependencies
const middlewareFunctions = middlewares . filter (( m ) => {
if (( m . before === m . after ) === null ) return true ;
const dependencies = ( m . before || []). concat ( m . after || []);
let flag = true ;
dependencies . forEach (( d ) => {
if (
flag === false ||
middlewares . findIndex (
( e ) =>
e . id === d &&
( e . scope === 'app' ||
e . scope === 'admin' ||
e . scope === 'frontStore' ||
e . routeId === null ||
e . routeId === m . scope ||
e . routeId === m . routeId )
) === - 1
) {
flag = false ;
}
});
return flag ;
});
// Sort using topological sorting
const sorter = new Topo . Sorter ();
middlewareFunctions . forEach (( m ) => {
sorter . add ( m . id , {
before: m . before ,
after: m . after ,
group: m . id
});
});
return sorter . nodes . map (( n ) => {
const index = middlewareFunctions . findIndex (( m ) => m . id === n );
const m = middlewareFunctions [ index ];
middlewareFunctions . splice ( index , 1 );
return m ;
});
}
Middleware Execution
The Handler Class
packages/evershop/src/lib/middleware/Handler.js
export class Handler {
static middleware () {
return ( request , response , next ) => {
// Merge custom params
request . params = {
... (( request . locals ?. customParams ) || {}),
... request . params
};
const { currentRoute } = request ;
let middlewares ;
if ( ! currentRoute ) {
middlewares = this . getAppLevelMiddlewares ( 'pages' );
} else {
middlewares = this . getMiddlewareByRoute ( currentRoute );
}
// Separate normal and error handlers
const goodHandlers = middlewares . filter (
( m ) => m . middleware . length === 3
);
const errorHandlers = middlewares . filter (
( m ) => m . middleware . length === 4
);
let currentGood = 0 ;
let currentError = - 1 ;
const eNext = function eNext () {
// All normal middlewares done
if ( arguments . length === 0 && currentGood === goodHandlers . length - 1 ) {
next ();
}
// All error handlers done
else if ( currentError === errorHandlers . length - 1 ) {
next ( arguments [ 0 ]);
}
// Error occurred, call error handler
else if ( arguments . length > 0 ) {
if ( ! isErrorHandlerTriggered ( response )) {
currentError += 1 ;
const middlewareFunc = errorHandlers [ currentError ]. middleware ;
middlewareFunc ( arguments [ 0 ], request , response , eNext );
}
}
// Call next normal middleware
else {
currentGood += 1 ;
const middlewareFunc = goodHandlers [ currentGood ]. middleware ;
middlewareFunc ( request , response , eNext );
}
};
// Start the chain
const { middleware } = goodHandlers [ 0 ];
middleware ( request , response , eNext );
};
}
}
Handler . middlewares = [];
Handler . sortedMiddlewarePerRoute = {};
Execution Flow
Request arrives
Route matching - Find the matching route
Middleware selection - Get middlewares for the route
Separation - Split into normal and error handlers
Execution - Execute normal handlers sequentially
Error handling - If error occurs, execute error handlers
Response - Send response to client
Middleware Scopes
Route-Specific Middleware
Placed directly in the route directory:
api/createProduct/
├── route.json
├── [auth]validate.js # Only for this route
└── createProduct.js
Area-Level Middleware
Applies to all routes in an area:
pages/admin/all/
└── checkPermission.js # All admin routes
pages/frontStore/all/
└── loadCart.js # All frontend routes
Global Middleware
Applies to all routes:
pages/global/
├── context.js # Always first
├── auth.js # Authentication
└── errorHandler.js # Error handling
Middleware Selection Logic
packages/evershop/src/lib/middleware/index.js
export function getModuleMiddlewares ( path ) {
if ( existsSync ( resolve ( path , 'pages' ))) {
// Scan for global middleware
if ( existsSync ( resolve ( path , 'pages' , 'global' ))) {
scanForMiddlewareFunctions (
resolve ( path , 'pages' , 'global' )
). forEach (( m ) => {
addMiddleware ( m );
});
}
// Scan for admin middleware
if ( existsSync ( resolve ( path , 'pages' , 'admin' ))) {
const routes = readdirSync ( resolve ( path , 'pages' , 'admin' ), {
withFileTypes: true
})
. filter (( dirent ) => dirent . isDirectory ())
. map (( dirent ) => dirent . name );
routes . forEach (( route ) => {
scanForMiddlewareFunctions (
resolve ( path , 'pages' , 'admin' , route )
). forEach (( m ) => {
addMiddleware ( m );
});
});
}
// Scan for frontStore middleware
if ( existsSync ( resolve ( path , 'pages' , 'frontStore' ))) {
const routes = readdirSync ( resolve ( path , 'pages' , 'frontStore' ), {
withFileTypes: true
})
. filter (( dirent ) => dirent . isDirectory ())
. map (( dirent ) => dirent . name );
routes . forEach (( route ) => {
scanForMiddlewareFunctions (
resolve ( path , 'pages' , 'frontStore' , route )
). forEach (( m ) => {
addMiddleware ( m );
});
});
}
}
// Scan for API middleware
if ( existsSync ( resolve ( path , 'api' ))) {
const routes = readdirSync ( resolve ( path , 'api' ), {
withFileTypes: true
})
. filter (( dirent ) => dirent . isDirectory ())
. map (( dirent ) => dirent . name );
routes . forEach (( route ) => {
scanForMiddlewareFunctions (
resolve ( path , 'api' , route )
). forEach (( m ) => {
addMiddleware ( m );
});
});
}
}
Practical Examples
Example 1: Authentication Middleware
import { verify } from 'jsonwebtoken' ;
export default async function auth ( request , response , next ) {
const token = request . headers . authorization ?. replace ( 'Bearer ' , '' );
if ( token ) {
try {
const decoded = verify ( token , process . env . JWT_SECRET );
request . user = decoded ;
} catch ( error ) {
// Invalid token, continue without user
}
}
next ();
}
Example 2: Validation Middleware
api/createProduct/[auth]validate.js
import { z } from 'zod' ;
const productSchema = z . object ({
name: z . string (). min ( 1 ),
sku: z . string (). min ( 1 ),
price: z . number (). positive (),
status: z . number (). optional ()
});
export default async function validate ( request , response , next ) {
try {
productSchema . parse ( request . body );
next ();
} catch ( error ) {
response . status ( 400 ). json ({
success: false ,
message: 'Validation failed' ,
errors: error . errors
});
}
}
Example 3: Data Loading Middleware
pages/frontStore/productView/loadProduct.js
import { select } from '@evershop/postgres-query-builder' ;
export default async function loadProduct ( request , response , next ) {
const { id } = request . params ;
const product = await select ()
. from ( 'product' )
. where ( 'uuid' , '=' , id )
. load ( pool );
if ( ! product ) {
response . status ( 404 );
// Will trigger notFound route
return ;
}
request . product = product ;
next ();
}
Example 4: Response Middleware
api/createProduct/[validate]createProduct.js
import { insert } from '@evershop/postgres-query-builder' ;
import { emit } from '@evershop/evershop/src/lib/event/emitter' ;
export default async function createProduct ( request , response ) {
const data = request . body ;
try {
const product = await insert ( 'product' )
. given ( data )
. execute ( pool );
// Emit event
await emit ( 'product_created' , { product });
response . json ({
success: true ,
data: product
});
} catch ( error ) {
response . status ( 500 ). json ({
success: false ,
message: error . message
});
}
}
Example 5: Error Handler
pages/global/errorHandler.js
export default function errorHandler ( error , request , response , next ) {
console . error ( 'Error:' , error );
// Don't expose internal errors in production
const message = process . env . NODE_ENV === 'production'
? 'Internal server error'
: error . message ;
response . status ( 500 ). json ({
success: false ,
message
});
}
Advanced Patterns
Conditional Execution
export default async function conditionalMiddleware ( request , response , next ) {
if ( request . path . startsWith ( '/api/admin' )) {
// Check admin permissions
if ( ! request . user ?. isAdmin ) {
response . status ( 403 ). json ({ error: 'Forbidden' });
return ;
}
}
next ();
}
Async Data Loading
export default async function loadRelatedData ( request , response , next ) {
const { product } = request ;
// Load related data in parallel
const [ category , variants , reviews ] = await Promise . all ([
loadCategory ( product . category_id ),
loadVariants ( product . product_id ),
loadReviews ( product . product_id )
]);
request . product . category = category ;
request . product . variants = variants ;
request . product . reviews = reviews ;
next ();
}
export default function transformRequest ( request , response , next ) {
// Sanitize input
if ( request . body ) {
Object . keys ( request . body ). forEach ( key => {
if ( typeof request . body [ key ] === 'string' ) {
request . body [ key ] = request . body [ key ]. trim ();
}
});
}
next ();
}
Best Practices
Each middleware should do one thing well. Don’t combine authentication, validation, and business logic in one function.
Middleware IDs should clearly indicate their purpose: auth, validate, loadProduct, not middleware1, handler, etc.
Always handle errors in middleware. Either pass them to next(error) or send an error response.
Don't forget to call next()
Unless you’re sending a response, always call next() to continue the chain. Forgetting this will hang the request.
Only specify dependencies that are truly required. Over-specifying can make the middleware chain rigid.
Never send multiple responses in the same middleware chain. Once you call response.json() or response.send(), don’t call next() unless you want to trigger an error.
Next Steps
Events Learn about the event system for decoupled communication
Routing Revisit routing to see how it integrates with middleware