The reuse and modularization of middleware
Reusability and Modularization of Middleware
Express middleware is the core mechanism for handling HTTP requests. By breaking down functionality into reusable modules, it can significantly improve code maintainability and development efficiency. Proper middleware design makes routing logic clearer, avoids code duplication, and facilitates team collaboration.
Basic Reusability Patterns for Middleware
Express middleware is essentially a function with a specific signature. The simplest way to reuse it is to extract the middleware function as an independent module. For example, a middleware that records request time can be encapsulated as follows:
// middleware/requestTime.js
module.exports = function requestTime(req, res, next) {
req.requestTime = Date.now()
next()
}
// app.js
const requestTime = require('./middleware/requestTime')
app.use(requestTime)
This pattern is particularly suitable for logic that needs to be reused across multiple routes, such as authentication, data preprocessing, etc. More complex middleware can be configured using factory functions:
// middleware/logger.js
module.exports = function(format) {
return function(req, res, next) {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`)
next()
}
}
// app.js
const logger = require('./middleware/logger')
app.use(logger('combined'))
Modular Middleware Composition
Multiple related middleware can be combined into a module package and exported uniformly via index.js
. For example, building a collection of API security-related middleware:
// middleware/security/index.js
const helmet = require('helmet')
const rateLimit = require('express-rate-limit')
module.exports = {
basicProtection: helmet(),
apiLimiter: rateLimit({
windowMs: 15 * 60 * 1000,
max: 100
}),
cors: require('./cors')
}
It can be imported as a whole or selectively:
const { basicProtection, apiLimiter } = require('./middleware/security')
app.use(basicProtection)
app.use('/api/', apiLimiter)
Route-Level Middleware Encapsulation
For middleware specific to a group of routes, modularization can be achieved more finely using Router
. For example, a user system routing module:
// routes/users.js
const router = require('express').Router()
const auth = require('../middleware/auth')
const validator = require('../middleware/userValidator')
router.use(auth.required)
router.get('/profile', validator.getProfile, getUserProfile)
router.post('/update', validator.updateUser, updateUser)
module.exports = router
The main application file only needs to mount the routing module:
// app.js
app.use('/users', require('./routes/users'))
Dynamic Middleware Loading Mechanism
Dynamic loading of middleware via configuration files enables environment-specific management. Create middleware-loader.js
:
const fs = require('fs')
const path = require('path')
module.exports = function(app) {
const env = process.env.NODE_ENV || 'development'
const config = require(`./config/${env}.json`)
config.middlewares.forEach(mw => {
const middleware = require(path.join(__dirname, mw.path))
app.use(middleware(config.options[mw.name]))
})
}
Example configuration file:
// config/development.json
{
"middlewares": [
{ "name": "logger", "path": "./middleware/logger" },
{ "name": "debug", "path": "./middleware/debugTool" }
],
"options": {
"logger": { "level": "verbose" }
}
}
Middleware Testing Strategies
Reusable middleware should be independently testable. Example of HTTP-layer testing using supertest
:
// test/authMiddleware.test.js
const request = require('supertest')
const express = require('express')
const auth = require('../middleware/auth')
test('auth middleware rejects missing token', async () => {
const app = express()
app.get('/protected', auth.required, (req, res) => res.sendStatus(200))
const res = await request(app)
.get('/protected')
.expect(401)
expect(res.body.error).toBe('Unauthorized')
})
For pure function middleware, direct invocation testing can be used:
// test/queryParser.test.js
const queryParser = require('../middleware/queryParser')
test('transforms comma-separated strings to arrays', () => {
const req = { query: { tags: 'js,node,express' } }
const next = jest.fn()
queryParser(req, {}, next)
expect(req.query.tags).toEqual(['js', 'node', 'express'])
expect(next).toHaveBeenCalled()
})
Error Handling Patterns for Middleware
Error-handling middleware requires special design considerations. Creating a reusable error handler:
// middleware/errorHandler.js
module.exports = function(options = {}) {
return function(err, req, res, next) {
if (options.log) {
console.error('[ERROR]', err.stack)
}
res.status(err.status || 500).json({
error: options.expose ? err.message : 'Internal Server Error'
})
}
}
// app.js
const errorHandler = require('./middleware/errorHandler')
app.use(errorHandler({
expose: process.env.NODE_ENV === 'development'
}))
For async middleware, wrapping is required:
// middleware/asyncHandler.js
module.exports = function(handler) {
return function(req, res, next) {
Promise.resolve(handler(req, res, next))
.catch(next)
}
}
// routes/users.js
router.get('/:id', asyncHandler(async (req, res) => {
const user = await User.findById(req.params.id)
if (!user) throw new Error('Not found')
res.json(user)
}))
Performance Optimization for Middleware
High-frequency middleware should consider performance optimization. For example, caching ETag calculations for static resources:
// middleware/staticCache.js
const crypto = require('crypto')
const fs = require('fs').promises
module.exports = function(root) {
const cache = new Map()
return async function(req, res, next) {
if (req.method !== 'GET') return next()
const filePath = path.join(root, req.path)
try {
let etag = cache.get(filePath)
const stats = await fs.stat(filePath)
if (!etag || etag.mtime < stats.mtimeMs) {
const content = await fs.readFile(filePath)
etag = {
value: crypto.createHash('md5').update(content).digest('hex'),
mtime: stats.mtimeMs
}
cache.set(filePath, etag)
}
res.set('ETag', etag.value)
if (req.headers['if-none-match'] === etag.value) {
return res.sendStatus(304)
}
} catch (err) {
// Pass through errors like file not found
}
next()
}
}
Type Safety for Middleware
In TypeScript projects, middleware type constraints can be defined:
// types/middleware.ts
import { Request, Response, NextFunction } from 'express'
export interface Middleware {
(req: Request, res: Response, next: NextFunction): void
}
export interface AsyncMiddleware {
(req: Request, res: Response, next: NextFunction): Promise<void>
}
// middleware/auth.ts
const authMiddleware: Middleware = (req, res, next) => {
if (!req.headers.authorization) {
return res.status(401).json({ error: 'Unauthorized' })
}
next()
}
Dependency Injection for Middleware
For middleware requiring external dependencies, a dependency injection pattern can be adopted:
// middleware/dbContext.js
module.exports = function(db) {
return function(req, res, next) {
req.db = {
users: db.collection('users'),
posts: db.collection('posts')
}
next()
}
}
// app.js
const MongoClient = require('mongodb').MongoClient
const dbContext = require('./middleware/dbContext')
MongoClient.connect(uri, (err, client) => {
const db = client.db('myapp')
app.use(dbContext(db))
})
AOP Practices in Middleware
Aspect-oriented programming is particularly suitable for middleware. For example, implementing method execution time measurement:
// middleware/benchmark.js
module.exports = function(label) {
return function(req, res, next) {
const start = process.hrtime()
res.on('finish', () => {
const diff = process.hrtime(start)
console.log(`${label} took ${diff[0] * 1e3 + diff[1] / 1e6}ms`)
})
next()
}
}
// routes/api.js
router.use(require('../middleware/benchmark')('API'))
router.get('/data', /* ... */)
Version Compatibility for Middleware
Middleware design for handling API version compatibility:
// middleware/versioning.js
module.exports = function(options) {
return function(req, res, next) {
const version = req.get('X-API-Version') || options.default
if (!options.supported.includes(version)) {
return res.status(406).json({
error: `Unsupported API version. Supported: ${options.supported.join(', ')}`
})
}
req.apiVersion = version
res.set('X-API-Version', version)
next()
}
}
// app.js
app.use(require('./middleware/versioning')({
default: 'v1',
supported: ['v1', 'v2']
}))
Metaprogramming Applications in Middleware
Using Proxy
to implement dynamic middleware properties:
// middleware/featureToggle.js
module.exports = function(features) {
return function(req, res, next) {
req.features = new Proxy({}, {
get(target, name) {
return features.includes(name)
? require(`./features/${name}`)
: null
}
})
next()
}
}
// routes/admin.js
router.post('/audit', (req, res) => {
if (req.features.advancedAudit) {
req.features.advancedAudit.log(req.body)
}
// ...
})
Lifecycle Extension for Middleware
Adding lifecycle hooks to middleware:
// middleware/lifecycle.js
module.exports = function() {
const hooks = {
pre: [],
post: []
}
const middleware = function(req, res, next) {
runHooks('pre', req)
.then(() => next())
.then(() => runHooks('post', req))
.catch(next)
}
middleware.hook = function(type, fn) {
hooks[type].push(fn)
return this
}
async function runHooks(type, req) {
for (const hook of hooks[type]) {
await hook(req)
}
}
return middleware
}
// app.js
const lifecycle = require('./middleware/lifecycle')()
lifecycle.hook('pre', req => console.log('Request started'))
lifecycle.hook('post', req => console.log('Request completed'))
app.use(lifecycle)
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn
上一篇:异步中间件的实现方式
下一篇:常用中间件库推荐与比较