Conditional Express Middleware Skipping
express-unless is a utility library for Express.js that enables conditional execution of middleware functions. It allows developers to attach an `unless` method directly to any Express middleware, providing granular control over when that middleware should be skipped. The library currently ships as version 2.1.3 and has seen stable releases, with the latest update in April 2021. It differentiates itself by offering a fluent API for defining skip conditions based on HTTP methods, request paths (including regular expressions and path/method pairs), file extensions, or a fully custom predicate function. This approach simplifies complex conditional routing logic often found in Express applications, providing a cleaner way to apply authentication, logging, or other cross-cutting concerns selectively.
Common errors
-
TypeError: Cannot read properties of undefined (reading 'unless')
cause The `unless` function from `express-unless` has not been correctly assigned to the middleware function, or the middleware function itself is undefined.fixEnsure that `mymid.unless = unless;` (or `(myMiddleware as any).unless = unless;` in TypeScript) is executed before attempting to call `mymid.unless(...)`. Verify `mymid` is a valid function. -
Middleware is always running/never running despite `unless` configuration.
cause The conditions specified in the `unless` options (e.g., `path`, `method`, `ext`, `custom`) are not evaluating as expected, causing the middleware to either always execute or always be skipped.fixReview the `unless` options: ensure `path` regular expressions are correct, `method` arrays include all intended methods, `ext` matches actual file extensions, and `custom` functions return the expected boolean value. Use `console.log` statements within a `custom` function to debug its return value and `req.originalUrl`/`req.url` values. Remember `unless` means 'skip if condition is met'.
Warnings
- gotcha The `unless` function extends existing middleware objects by adding an 'unless' property. This pattern, while effective, modifies foreign objects and could potentially conflict with other libraries or internal middleware properties if not handled carefully. Use type assertions in TypeScript to accommodate this design.
- gotcha The `path` option for `unless` matches against `req.originalUrl` by default. If you intend to match against the URL excluding router mounts, set `useOriginalUrl: false` to match against `req.url` instead. Misunderstanding this distinction is a common source of routing errors in Express.
- gotcha The behavior of `unless` is sensitive to middleware order. If a preceding middleware modifies `req.url` or `req.originalUrl`, or if `unless` itself is applied before a router, it can lead to unexpected conditional skips or runs.
Install
-
npm install express-unless -
yarn add express-unless -
pnpm add express-unless
Imports
- unless
import unless from 'express-unless';
import { unless } from 'express-unless'; - unless
const { unless } = require('express-unless'); - UnlessOptions
import type { UnlessOptions } from 'express-unless';
Quickstart
import express from 'express';
import { unless } from 'express-unless';
const app = express();
const port = 3000;
// 1. A middleware to simulate authentication
const requiresAuth = (req: express.Request, res: express.Response, next: express.NextFunction) => {
if (req.headers.authorization === 'Bearer my-secret-token') {
console.log('User authenticated.');
next();
} else {
console.warn('Authentication failed for:', req.originalUrl);
res.status(401).send('Unauthorized');
}
};
// Extend the middleware with unless (type assertion for TypeScript compatibility)
(requiresAuth as any).unless = unless;
// Apply authentication to all routes UNLESS they are '/public', '/status' or OPTIONS requests
app.use(
(requiresAuth as any).unless({
path: ['/public', '/status'],
method: ['OPTIONS']
})
);
// 2. A simple logger middleware
const logger = (req: express.Request, res: express.Response, next: express.NextFunction) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.originalUrl}`);
next();
};
(logger as any).unless = unless;
// Skip logging for image files or paths including '/no-log'
app.use(logger.unless({
ext: ['.png', '.jpg', '.gif'],
custom: (req: express.Request) => req.originalUrl.includes('/no-log')
}));
// Public route - no auth needed
app.get('/public', (req, res) => {
res.send('This is a public page.');
});
// Status route - no auth needed
app.get('/status', (req, res) => {
res.send('Server is healthy.');
});
// Authenticated route
app.get('/admin', (req, res) => {
res.send('Welcome to the admin area!');
});
// Route with images (logging skipped)
app.get('/images/test.png', (req, res) => {
res.send('Serving image.');
});
// Route where logging is skipped via custom function
app.get('/no-log/data', (req, res) => {
res.send('This route will not be logged.');
});
// Default route
app.get('/', (req, res) => {
res.send('Hello World!');
});
app.listen(port, () => {
console.log(`Express app listening at http://localhost:${port}`);
console.log('Try:');
console.log(' GET http://localhost:3000/ - Authenticated');
console.log(' GET http://localhost:3000/public - Not Authenticated');
console.log(' GET http://localhost:3000/admin - Authenticated (requires Authorization: Bearer my-secret-token)');
console.log(' OPTIONS http://localhost:3000/admin - Not Authenticated (skipped by method)');
console.log(' GET http://localhost:3000/images/test.png - Logging skipped');
console.log(' GET http://localhost:3000/no-log/data - Logging skipped by custom function');
});