中间件模型

req → middleware1 → middleware2 → handler → res
                ↓             ↓
              next()        next()

每个中间件接 (req, res, next),可以:

  • 改 req / res
  • next() 交给下一个
  • 直接响应(不调 next)
  • next(err) 跳到错误处理

基础

app.use((req, res, next) => {
    console.log(`${req.method} ${req.url}`);
    next();                          // ★ 别忘了
});

app.get('/', (req, res) => res.send('home'));

中间件能干什么

// 1. 日志
app.use((req, res, next) => {
    console.log(`[${new Date()}] ${req.method} ${req.url}`);
    next();
});

// 2. 计时
app.use((req, res, next) => {
    const start = Date.now();
    res.on('finish', () => {
        console.log(`${req.url} took ${Date.now() - start}ms`);
    });
    next();
});

// 3. 鉴权
function auth(req, res, next) {
    const token = req.headers.authorization?.replace('Bearer ', '');
    if (!token) return res.status(401).send('Missing token');
    try {
        req.user = jwt.verify(token, SECRET);
        next();
    } catch {
        res.status(401).send('Invalid token');
    }
}

app.get('/me', auth, (req, res) => res.json(req.user));

// 4. CORS
app.use((req, res, next) => {
    res.set('Access-Control-Allow-Origin', '*');
    res.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
    if (req.method === 'OPTIONS') return res.sendStatus(204);
    next();
});

// 5. 限流(生产用 express-rate-limit)
import rateLimit from 'express-rate-limit';
app.use('/api', rateLimit({ windowMs: 60 * 1000, max: 100 }));

路径前缀

app.use('/api', logRequests);            // 只对 /api/* 生效
app.use('/admin', requireAuth, adminRouter);

几个必装包

作用
morgan 请求日志(HTTP combined / dev 格式)
helmet 加一组安全 HTTP header
cors CORS 跨域
compression gzip 响应
express-rate-limit 限流
cookie-parser 解析 cookie
express-session session
multer 上传文件
import morgan from 'morgan';
import helmet from 'helmet';
import cors from 'cors';
import compression from 'compression';

app.use(helmet());                      // 安全 header
app.use(cors());                         // 跨域
app.use(compression());                  // gzip
app.use(morgan('dev'));                  // 日志
app.use(express.json());

错误处理中间件

4 个参数的中间件是错误处理器:

app.use((err, req, res, next) => {
    //   ^^^ 4 个参数
    console.error(err);
    res.status(err.status || 500).json({ error: err.message });
});

放在所有路由和中间件之后(最后)。

自定义错误类

class HttpError extends Error {
    constructor(status, message) {
        super(message);
        this.status = status;
    }
}

// 用
app.get('/users/:id', async (req, res, next) => {
    const user = await db.users.find(req.params.id);
    if (!user) throw new HttpError(404, 'User not found');
    res.json(user);
});

// 全局处理
app.use((err, req, res, next) => {
    if (err instanceof HttpError) {
        return res.status(err.status).json({ error: err.message });
    }
    console.error(err);
    res.status(500).json({ error: 'Internal error' });
});

404 兜底

// 放在所有路由之后
app.use((req, res) => {
    res.status(404).send('Not Found');
});

一份生产级骨架

import express from 'express';
import morgan from 'morgan';
import helmet from 'helmet';
import cors from 'cors';
import compression from 'compression';
import rateLimit from 'express-rate-limit';

const app = express();

app.set('trust proxy', 1);

// 通用中间件
app.use(helmet());
app.use(cors({ origin: process.env.CORS_ORIGIN || '*' }));
app.use(compression());
app.use(morgan(process.env.NODE_ENV === 'production' ? 'combined' : 'dev'));
app.use(express.json({ limit: '1mb' }));

// 限流
app.use('/api', rateLimit({ windowMs: 60_000, max: 100 }));

// 健康检查
app.get('/health', (req, res) => res.json({ ok: true }));

// 业务路由
app.use('/api/users', usersRouter);
app.use('/api/posts', postsRouter);

// 404
app.use((req, res) => res.status(404).json({ error: 'Not Found' }));

// 错误处理(最后)
app.use((err, req, res, next) => {
    console.error(err);
    const status = err.status || 500;
    res.status(status).json({
        error: status >= 500 ? 'Internal error' : err.message,
    });
});

app.listen(process.env.PORT || 3000);

  • 中间件顺序很重要——先注册先跑
  • 调了 next() 之后还 res.send() 会报错(headers already sent)
  • 错误处理中间件必须 4 个参数——少一个就不被识别为错误处理器
  • 异步中间件忘 await/return 可能让请求挂起

下一篇:Fastify。