停止编写 Spaghetti API 路由 — 像专家一样构建你的 Express.js 应用

发布: (2026年3月1日 GMT+8 20:06)
11 分钟阅读
原文: Dev.to

抱歉,我无法直接访问外部链接获取文章内容。请您把需要翻译的文本粘贴在这里,我会帮您翻译成简体中文,并保留原有的格式和代码块。

面条式 API 问题

每个开发者都经历过这种情况。你带着最好的意图启动一个新的 Express.js 项目:一个干净的 index.js,也许两个路由,还有一个梦想。然后三个月后,你打开文件,看到 800 行路由处理函数,层层叠叠的中间件,以及到处散落的数据库调用,像糟糕派对后的彩纸一样。

这就是 面条式 API 问题,它导致的项目失败比 bug 还多。

在本文中,我将带你了解一种经过实战检验的 Express.js 文件夹结构,它可以从周末的副项目扩展到生产级应用,而不会变成维护噩梦。

为什么结构比你想象的更重要

  • 糟糕的结构不仅仅是审美问题。它会主动拖慢团队速度,使新人入职变得痛苦,并把“添加一个简单功能”变成三小时的考古挖掘。
  • 良好的结构恰恰相反。它让代码放置的正确位置一目了然,分离关注点,使得某一区域的更改不会意外波及其他区域,并且让你在扩展团队和代码库时不至于四分五裂。

让我们从头开始构建。

文件夹结构

以下是我们将实现的结构:

my-api/
  src/
    config/
      index.js
      database.js
    controllers/
      userController.js
      postController.js
    middlewares/
      auth.js
      errorHandler.js
      validate.js
    models/
      User.js
      Post.js
    routes/
      index.js
      userRoutes.js
      postRoutes.js
    services/
      userService.js
      postService.js
    utils/
      logger.js
      response.js
  app.js
  server.js

第 1 层 – 配置

所有环境变量和配置都放在这里。不再有 process.env.DATABASE_URL 分散在 15 个文件中。

// src/config/index.js
module.exports = {
  port: process.env.PORT || 3000,
  nodeEnv: process.env.NODE_ENV || 'development',
  jwtSecret: process.env.JWT_SECRET,
  db: {
    url: process.env.DATABASE_URL,
    poolSize: parseInt(process.env.DB_POOL_SIZE, 10) || 5,
  },
};

当你的数据库 URL 变化时,只需更新 一个 文件。完成。

第 2 层 – 路由

路由是流量指挥员。它们不应包含 业务逻辑——只负责将 HTTP 方法/路径映射到控制器函数。

// src/routes/userRoutes.js
const express = require('express');
const router = express.Router();
const { authenticate } = require('../middlewares/auth');
const { validateCreateUser } = require('../middlewares/validate');
const userController = require('../controllers/userController');

router.get('/', authenticate, userController.getUsers);
router.post('/', validateCreateUser, userController.createUser);
router.get('/:id', authenticate, userController.getUserById);
router.put('/:id', authenticate, userController.updateUser);
router.delete('/:id', authenticate, userController.deleteUser);

module.exports = router;

所有路由组在单个 index 文件中组合:

// src/routes/index.js
const express = require('express');
const router = express.Router();
const userRoutes = require('./userRoutes');
const postRoutes = require('./postRoutes');

router.use('/users', userRoutes);
router.use('/posts', postRoutes);

module.exports = router;

第 3 层 – 控制器

控制器处理 HTTP 层:它们接收请求,调用相应的服务,并返回响应。没有数据库调用,也没有复杂逻辑。

// src/controllers/userController.js
const userService = require('../services/userService');
const { sendSuccess, sendError } = require('../utils/response');

exports.getUsers = async (req, res, next) => {
  try {
    const { page = 1, limit = 20 } = req.query;
    const users = await userService.getUsers({ page, limit });
    return sendSuccess(res, users);
  } catch (error) {
    next(error);
  }
};

exports.createUser = async (req, res, next) => {
  try {
    const user = await userService.createUser(req.body);
    return sendSuccess(res, user, 201);
  } catch (error) {
    next(error);
  }
};

exports.getUserById = async (req, res, next) => {
  try {
    const user = await userService.getUserById(req.params.id);
    if (!user) return sendError(res, 'User not found', 404);
    return sendSuccess(res, user);
  } catch (error) {
    next(error);
  }
};

第 4 层 – 服务

业务逻辑位于此。服务是普通的 JavaScript——没有 req、没有 res、没有 HTTP 概念——因此极易测试。

// src/services/userService.js
const User = require('../models/User');
const { hashPassword } = require('../utils/crypto');

exports.getUsers = async ({ page, limit }) => {
  const offset = (page - 1) * limit;
  const users = await User.findAll({
    limit: parseInt(limit, 10),
    offset,
    attributes: { exclude: ['password'] },
  });
  return users;
};

exports.createUser = async (userData) => {
  const existing = await User.findOne({ where: { email: userData.email } });
  if (existing) {
    const error = new Error('Email already in use');
    error.statusCode = 409;
    throw error;
  }

  const hashed = await hashPasswo

```js
rd(userData.password);
  const user = await User.create({ ...userData, password: hashed });

  const { password, ...userWithoutPassword } = user.toJSON();
  return userWithoutPassword;
};

exports.getUserById = async (id) => {
  return User.findByPk(id, {
    attributes: { exclude: ['password'] },
  });
};

第 5 层 – 模型

定义数据结构和 ORM 映射(例如 Sequelize、Mongoose)。保持它们不包含与请求相关的代码。

// src/models/User.js
const { DataTypes } = require('sequelize');
const sequelize = require('../config/database');

const User = sequelize.define('User', {
  id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
  email: { type: DataTypes.STRING, unique: true, allowNull: false },
  password: { type: DataTypes.STRING, allowNull: false },
  name: { type: DataTypes.STRING },
  // ...other fields
});

module.exports = User;

第 6 层 – 中间件

在请求与控制器之间运行的可复用逻辑(认证、验证、错误处理等)。

// src/middlewares/auth.js
module.exports.authenticate = (req, res, next) => {
  // 验证 JWT,向 req 附加用户等
  next();
};
// src/middlewares/validate.js
module.exports.validateCreateUser = (req, res, next) => {
  // 执行 Joi/Yup 验证
  next();
};
// src/middlewares/errorHandler.js
module.exports = (err, req, res, next) => {
  const status = err.statusCode || 500;
  res.status(status).json({ error: err.message });
};

第 7 层 – 工具函数

不适合放在其他位置的辅助函数(日志、响应格式化、加密帮助等)。

// src/utils/response.js
exports.sendSuccess = (res, data, status = 200) => {
  res.status(status).json({ success: true, data });
};

exports.sendError = (res, message, status = 500) => {
  res.status(status).json({ success: false, error: message });
};
// src/utils/logger.js
const winston = require('winston');
module.exports = winston.createLogger({
  transports: [new winston.transports.Console()],
});

将所有内容连接在一起

// app.js
const express = require('express');
const config = require('./src/config');
const routes = require('./src/routes');
const errorHandler = require('./src/middlewares/errorHandler');

const app = express();

app.use(express.json());
app.use('/api', routes);
app.use(errorHandler);

module.exports = app;
// server.js
const http = require('http');
const app = require('./app');
const config = require('./src/config');

const server = http.createServer(app);

server.listen(config.port, () => {
  console.log(`🚀 Server running on http://localhost:${config.port}`);
});

TL;DR

  1. Config – 集中的环境设置。
  2. Routes – 纯粹的 HTTP 路径 → 控制器映射。
  3. Controllers – 轻量级请求/响应处理器。
  4. Services – 业务逻辑,完全与框架无关。
  5. Models – 数据库模式 / ORM 定义。
  6. Middlewares – 可复用的请求处理工具。
  7. Utils – 各种辅助工具(日志、响应格式化等)。

坚持这种分层方法,你的 Express.js 代码库将保持整洁、可测试且可扩展——不再是乱七八糟的代码。

// Example of a Sequelize query with excluded fields
User.findAll({
  attributes: { exclude: ['password'] },
});

第5层:Middlewares

中间件处理横切关注点:身份验证、校验、限流、日志记录。保持它们小而专注。

// src/middlewares/auth.js
const jwt = require('jsonwebtoken');
const config = require('../config');

exports.authenticate = (req, res, next) => {
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) {
    return res.status(401).json({ message: 'No token provided' });
  }

  try {
    const decoded = jwt.verify(token, config.jwtSecret);
    req.user = decoded;
    next();
  } catch {
    return res.status(401).json({ message: 'Invalid token' });
  }
};
// src/middlewares/errorHandler.js
module.exports = (err, req, res, next) => {
  const statusCode = err.statusCode || 500;
  const message = err.message || 'Internal Server Error';

  if (process.env.NODE_ENV !== 'production') {
    console.error(err.stack);
  }

  res.status(statusCode).json({
    success: false,
    message,
  });
};

全局错误处理器是此设置中不为人知的英雄。注意每个控制器只调用 next(error) —— 这个唯一的中间件会捕获所有错误并统一格式化响应。

将所有内容串联起来

// app.js
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const routes = require('./src/routes');
const errorHandler = require('./src/middlewares/errorHandler');

const app = express();

// Security and parsing
app.use(helmet());
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// All routes under /api/v1
app.use('/api/v1', routes);

// Health check
app.get('/health', (req, res) => {
  res.json({ status: 'ok', timestamp: new Date().toISOString() });
});

// Error handler must be last
app.use(errorHandler);

module.exports = app;
// server.js
const app = require('./app');
const config = require('./src/config');

app.listen(config.port, () => {
  console.log(`Server running on port ${config.port} in ${config.nodeEnv} mode`);
});

app.jsserver.js 分离是一个小而有力的做法。这意味着你的测试套件可以直接导入 app,而无需实际启动服务器,从而使集成测试更加简洁。

经验法则

当你不确定某件事应该放在哪里时,问问自己:

  • 它是否涉及 reqres?→ Controllermiddleware
  • 它是否包含业务逻辑?→ Service
  • 它是否与数据库交互并映射数据?→ Model
  • 它是否是 HTTP 路径?→ Route
  • 它是否在多个地方使用且没有副作用?→ Utility

只要遵循这五个问题,你几乎总能把它放在正确的位置。

这能为您带来什么

  • 测试变得轻松。 服务没有 HTTP 依赖,因此单元测试只是函数调用。
  • 新开发者快速上手。 文件夹名称讲述了应用的故事。
  • 功能是隔离的。 添加新资源意味着在每一层都添加文件——你永远不需要触及无关的代码。
  • 调试更快。 当出现 bug 时,你可以根据问题类型准确知道该查看哪一层。

结构不是官僚主义。它是构建持久事物的方式。

开始使用此布局重构你的下一个 Express.js 项目,看看一切变得多么轻松。你将花更少的时间寻找代码,更多的时间编写代码。

0 浏览
Back to Blog

相关文章

阅读更多 »

前5名 Node.js REST API 框架

快速概览 你是否想过,为什么科技行业的每个人都对 Node 疯狂追捧?从 Netflix 到 Uber 再到 LinkedIn,各公司都在努力…