构建一个“麻雀虽小,五脏俱全”的后端项目,核心在于职责分离(Separation of Concerns)。将路由、控制器、业务逻辑和数据访问解耦,可以极大提升代码的可维护性和可测试性。
为了满足你的需求,这里设计一个任务管理系统(Task API)。它使用 Express.js 构建,以 SQLite 为数据库(无需额外安装独立的数据库服务,方便运行),并引入了分层架构、全局错误处理、分页、数据库索引和简单的内存缓存。
1. 目录结构与工程化设计
在开始写代码前,先明确项目的骨架。这种经典的三层架构是现代后端工程化的基础:
Plaintext
express-task-api/
├── config/
│ └── db.js # 数据库连接与初始化
├── middlewares/
│ ├── errorHandler.js # 全局异常捕获
│ └── cache.js # 缓存中间件(性能优化)
├── models/
│ └── taskModel.js # 数据访问层 (DAO - 直接操作数据库)
├── services/
│ └── taskService.js # 业务逻辑层 (处理核心规则)
├── controllers/
│ └── taskController.js# 路由控制层 (解析请求与响应)
├── routes/
│ └── taskRoutes.js # 路由分发
├── app.js # Express 应用实例与中间件组装
├── server.js # 服务启动入口
└── package.json
2. 核心代码实现
你需要安装以下基础依赖:
npm install express sqlite3 sqlite cors
2.1 数据库配置与优化 (config/db.js)
这里使用了 SQLite,并在 status 字段上建立了索引。性能优化点: 当数据量变大时,按状态(如查询所有“未完成”任务)过滤时的全表扫描会被索引查找替代,时间复杂度从 降至 。
JavaScript
// config/db.js
const sqlite3 = require('sqlite3').verbose();
const { open } = require('sqlite');
async function initDB() {
const db = await open({
filename: './database.sqlite',
driver: sqlite3.Database
});
// 初始化表与索引
await db.exec(`
CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
status TEXT DEFAULT 'pending', -- pending, completed, archived
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 性能优化:为常用查询字段建立索引
CREATE INDEX IF NOT EXISTS idx_status ON tasks(status);
`);
return db;
}
module.exports = initDB;
2.2 数据访问层 (models/taskModel.js)
只负责和数据库打交道,不包含业务判断。
JavaScript
// models/taskModel.js
class TaskModel {
constructor(db) {
this.db = db;
}
// 性能优化:引入分页 (limit, offset)
async findTasks(limit, offset, status) {
let query = `SELECT * FROM tasks`;
const params = [];
if (status) {
query += ` WHERE status = ?`;
params.push(status);
}
query += ` ORDER BY created_at DESC LIMIT ? OFFSET ?`;
params.push(limit, offset);
return await this.db.all(query, params);
}
async findById(id) {
return await this.db.get(`SELECT * FROM tasks WHERE id = ?`, [id]);
}
async create(title) {
const result = await this.db.run(`INSERT INTO tasks (title) VALUES (?)`, [title]);
return result.lastID;
}
async updateStatus(id, status) {
await this.db.run(`UPDATE tasks SET status = ? WHERE id = ?`, [status, id]);
}
async delete(id) {
await this.db.run(`DELETE FROM tasks WHERE id = ?`, [id]);
}
}
module.exports = TaskModel;
2.3 业务逻辑层 (services/taskService.js)
经典业务逻辑体现: 状态流转限制。比如,已归档(archived)的任务不能直接被标记为完成。所有的业务异常都在这里抛出。
JavaScript
// services/taskService.js
class TaskService {
constructor(taskModel) {
this.taskModel = taskModel;
}
async getTasks(page = 1, pageSize = 10, status) {
const limit = parseInt(pageSize, 10);
const offset = (Math.max(1, page) - 1) * limit;
return await this.taskModel.findTasks(limit, offset, status);
}
async createTask(title) {
if (!title || title.trim() === '') {
throw new Error('Task title is required'); // 业务校验
}
return await this.taskModel.create(title.trim());
}
async updateTaskStatus(id, newStatus) {
const validStatuses = ['pending', 'completed', 'archived'];
if (!validStatuses.includes(newStatus)) {
throw new Error('Invalid status');
}
const task = await this.taskModel.findById(id);
if (!task) throw new Error('Task not found');
// 核心业务逻辑:已归档任务不可更改状态
if (task.status === 'archived') {
throw new Error('Cannot update an archived task');
}
await this.taskModel.updateStatus(id, newStatus);
return { id, status: newStatus };
}
async deleteTask(id) {
const task = await this.taskModel.findById(id);
if (!task) throw new Error('Task not found');
await this.taskModel.delete(id);
}
}
module.exports = TaskService;
2.4 中间件与控制器 (controllers & middlewares)
性能优化点: 简单的内存缓存。对于高频读取且不常变动的接口(例如统计信息或基础列表),缓存在内存中可以极大降低数据库 I/O。
JavaScript
// middlewares/cache.js
const memoryCache = new Map();
// 简单的中间件缓存实现
const simpleCache = (durationInSeconds) => {
return (req, res, next) => {
const key = req.originalUrl;
const cachedResponse = memoryCache.get(key);
if (cachedResponse && cachedResponse.expiry > Date.now()) {
return res.json(cachedResponse.data); // 命中缓存直接返回
}
// 劫持 res.json 以便在发送前存入缓存
const originalSend = res.json;
res.json = (body) => {
memoryCache.set(key, {
data: body,
expiry: Date.now() + durationInSeconds * 1000
});
originalSend.call(res, body);
};
next();
};
};
module.exports = simpleCache;
JavaScript
// middlewares/errorHandler.js
const errorHandler = (err, req, res, next) => {
console.error(`[Error] ${err.message}`);
// 统一的错误格式输出
res.status(err.status || 400).json({
success: false,
error: err.message || 'Internal Server Error'
});
};
module.exports = errorHandler;
JavaScript
// controllers/taskController.js
class TaskController {
constructor(taskService) {
this.taskService = taskService;
}
// 利用箭头函数绑定 this,或者在路由注册时使用 bind
getTasks = async (req, res, next) => {
try {
const { page, limit, status } = req.query;
const tasks = await this.taskService.getTasks(page, limit, status);
res.json({ success: true, data: tasks });
} catch (error) {
next(error); // 传递给全局错误处理中间件
}
};
createTask = async (req, res, next) => {
try {
const { title } = req.body;
const id = await this.taskService.createTask(title);
res.status(201).json({ success: true, data: { id, title } });
} catch (error) {
next(error);
}
};
updateStatus = async (req, res, next) => {
try {
const { id } = req.params;
const { status } = req.body;
const result = await this.taskService.updateTaskStatus(id, status);
res.json({ success: true, data: result });
} catch (error) {
next(error);
}
};
deleteTask = async (req, res, next) => {
try {
const { id } = req.params;
await this.taskService.deleteTask(id);
res.json({ success: true, message: 'Deleted successfully' });
} catch (error) {
next(error);
}
};
}
module.exports = TaskController;
2.5 路由与应用入口 (routes, app.js, server.js)
将各层进行依赖注入(Dependency Injection),这是保证模块可测试性的关键。
JavaScript
// routes/taskRoutes.js
const express = require('express');
const simpleCache = require('../middlewares/cache');
module.exports = (taskController) => {
const router = express.Router();
// 查询列表使用了 5 秒的短缓存,降低高并发下的 DB 压力
router.get('/', simpleCache(5), taskController.getTasks);
router.post('/', taskController.createTask);
router.patch('/:id/status', taskController.updateStatus);
router.delete('/:id', taskController.deleteTask);
return router;
};
JavaScript
// app.js
const express = require('express');
const cors = require('cors');
const errorHandler = require('./middlewares/errorHandler');
const taskRoutes = require('./routes/taskRoutes');
function createApp(taskController) {
const app = express();
app.use(cors());
app.use(express.json()); // 解析 JSON body
// 挂载路由
app.use('/api/tasks', taskRoutes(taskController));
// 全局错误处理垫后
app.use(errorHandler);
return app;
}
module.exports = createApp;
JavaScript
// server.js
const initDB = require('./config/db');
const TaskModel = require('./models/taskModel');
const TaskService = require('./services/taskService');
const TaskController = require('./controllers/taskController');
const createApp = require('./app');
const PORT = process.env.PORT || 3000;
async function startServer() {
try {
// 1. 初始化数据库
const db = await initDB();
// 2. 依赖注入组装应用
const taskModel = new TaskModel(db);
const taskService = new TaskService(taskModel);
const taskController = new TaskController(taskService);
const app = createApp(taskController);
// 3. 启动监听
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
} catch (err) {
console.error('Failed to start server:', err);
process.exit(1);
}
}
startServer();
3. 设计思路与解构分析
-
业务隔离的必要性:很多初学者习惯在 Controller 里写 SQL 或判断逻辑。这里的代码严格遵循了“Controller 只管请求调度,Service 只管业务规则,Model 只管数据读写”。如果未来要把 SQLite 换成 MySQL 或 PostgreSQL,只需要重写
TaskModel,上面的Service和Controller一行代码都不用改。 -
防御性编程:在
Service层对空标题、无效状态进行了拦截,并在状态流转上切断了非法路径(归档不可编辑)。 -
性能优化的切面:
-
数据库层:
idx_status索引解决了分类过滤时的性能瓶颈。 -
查询层:强制的分页处理(
limit/offset)防止一次请求打爆内存。 -
网络层:
simpleCache中间件以极低的成本实现了查询接口的防抖降压。
-
你可以直接运行 node server.js 来启动这个项目并用 Postman 或 cURL 测试对应的端点。你需要针对这套架构补充自动化测试(如 Jest)方面的代码吗?