先搭一个能落地的项目结构
后端项目的可维护性,从合理的目录结构开始。我见过很多新手把所有代码堆在app.js
里,后期改一个逻辑要翻几百行——这种结构肯定走不远。分享一个我在生产环境中用了3年的目录模板,直接抄作业:

your-project/
├─ src/ # 核心代码目录
│ ├─ controller/ # 请求处理层(接收参数、调用服务、返回响应)
│ ├─ service/ # 业务逻辑层(封装数据库操作、第三方接口调用)
│ ├─ model/ # 数据模型层(数据库表/集合映射,比如Sequelize/Mongoose模型)
│ ├─ middleware/ # 中间件(日志、权限校验、错误处理等通用逻辑)
│ ├─ config/ # 配置文件(数据库、端口、第三方密钥,区分开发/生产环境)
│ ├─ router/ # 路由定义(把URL映射到对应的controller)
│ └─ app.js # 应用入口(初始化框架、注册中间件/路由)
├─ .env # 环境变量(比如DB_URL、PORT)
├─ package.json # 依赖管理
└─ README.md # 项目说明
举个具体的例子:controller/user.js
负责处理用户相关的HTTP请求(比如GET /api/users
),它不直接操作数据库,而是调用service/user.js
里的getUserList
方法——这样业务逻辑和请求处理分离,后期要改数据库逻辑,只动service
层就行,不用碰controller
。
用Express/Koa2快速实现核心功能
Node.js生态里最常用的两个框架是Express(成熟、插件多)和Koa2(轻量、原生支持async/await
)。选哪个?看项目规模:如果是快速迭代的小项目,用Express;如果想写更简洁的异步代码,或者需要高度定制化,选Koa2。
Express示例:快速写一个用户列表接口
// src/router/user.js
const express = require('express');
const router = express.Router();
const userController = require('../controller/user');
// GET /api/users —— 获取用户列表
router.get('/users', userController.getUserList);
// GET /api/users/:id —— 获取单个用户
router.get('/users/:id', userController.getUserById);
module.exports = router;
// src/controller/user.js
const userService = require('../service/user');
exports.getUserList = async (req, res) => {
try {
const users = await userService.getUserList();
res.status(200).json({ code: 200, data: users });
} catch (err) {
res.status(500).json({ code: 500, message: err.message });
}
};
Koa2示例:用async/await
简化异步逻辑
Koa2没有内置路由,需要自己装koa-router
:
// src/app.js
const Koa = require('koa');
const router = require('koa-router')();
const userController = require('./controller/user');
const app = new Koa();
// 注册路由
router.get('/api/users', userController.getUserList);
router.get('/api/users/:id', userController.getUserById);
app.use(router.routes()).use(router.allowedMethods()); // 处理OPTIONS请求和405错误
app.listen(3000);
注意:Koa2的ctx
对象整合了req
和res
,比如ctx.params.id
对应URL里的:id
,ctx.body
用来设置响应体——比Express的req.params
+res.json
更简洁。
设计可维护的RESTful API
RESTful API不是“必须用GET查、POST增”的教条,而是用HTTP协议的语义表达资源操作。我总结了3条能直接落地的规则:
- 用名词表示资源:比如
/api/users
(用户列表)、/api/articles
(文章列表),不要用/api/getUsers
这种动词开头的URL。 - 用HTTP方法表示操作:
- GET:查询(比如
GET /api/users
获取所有用户,GET /api/users/1
获取ID=1的用户) - POST:新增(比如
POST /api/users
创建一个用户) - PUT:全量更新(比如
PUT /api/users/1
更新用户ID=1的所有信息) - PATCH:部分更新(比如
PATCH /api/users/1
只更新用户的邮箱) - DELETE:删除(比如
DELETE /api/users/1
删除用户ID=1) - 用状态码传递结果:别只用200和500!比如:
- 201 Created:新增成功(比如POST
/api/users
后返回) - 400 Bad Request:参数错误(比如没传
name
字段) - 401 Unauthorized:未登录(比如没传Token)
- 403 Forbidden:没有权限(比如普通用户想删管理员)
- 404 Not Found:资源不存在(比如
GET /api/users/999
)
举个正确的API响应示例:
// GET /api/users/1 成功
{
"code": 200,
"data": {
"id": 1,
"name": "Alice",
"email": "alice@example.com"
}
}
// POST /api/users 参数错误(没传name)
{
"code": 400,
"message": "name字段是必填项"
}
中间件:处理通用逻辑的利器
中间件是Node.js后端的“瑞士军刀”——把日志、权限校验、错误处理这些通用逻辑抽出来,不用在每个接口里重复写。
1. 日志中间件:跟踪请求链路
开发时想知道“谁在什么时候访问了哪个接口”?写个简单的日志中间件:
// src/middleware/logger.js(Express版)
module.exports = (req, res, next) => {
const timestamp = new Date().toISOString();
const method = req.method;
const url = req.url;
const ip = req.ip;
console.log(`[${timestamp}] ${method} ${url} - ${ip}`);
next(); // 一定要调用next(),否则请求会卡住
};
// 在src/app.js里注册
const logger = require('./middleware/logger');
app.use(logger);
Koa2版的日志中间件更灵活,还能统计请求耗时:
// src/middleware/logger.js(Koa2版)
module.exports = async (ctx, next) => {
const start = Date.now();
await next(); // 先执行后续中间件(比如路由处理)
const ms = Date.now() - start;
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
};
2. 错误处理中间件:统一捕获异常
后端代码里的try/catch
不要满天飞——用中间件统一处理:
// Express版:四个参数的函数是错误中间件
app.use((err, req, res, next) => {
console.error('错误栈:', err.stack);
// 根据错误类型返回不同状态码
if (err.name === 'ValidationError') { // Joi参数校验错误
return res.status(400).json({ code: 400, message: err.message });
}
res.status(500).json({ code: 500, message: '服务器打了个盹,请稍后再试' });
});
// Koa2版:用try/catch包裹next()
app.use(async (ctx, next) => {
try {
await next();
} catch (err) {
ctx.status = err.status || 500;
ctx.body = {
code: ctx.status,
message: err.message || '服务器内部错误'
};
}
});
数据库交互:从ORM到原生SQL
Node.js连接数据库,常用的方案有两种:ORM(对象关系映射)和原生SQL。
1. ORM:快速上手,避免SQL注入
ORM把数据库表映射成JavaScript对象,不用写SQL也能操作数据库。比如用Sequelize
(支持MySQL、PostgreSQL)操作MySQL:
// src/model/User.js
const { Sequelize, DataTypes } = require('sequelize');
const sequelize = new Sequelize(process.env.DB_URL); // 从环境变量取数据库地址
// 定义User模型(对应users表)
const User = sequelize.define('User', {
name: {
type: DataTypes.STRING(30), // 字符串,最长30字符
allowNull: false, // 不允许为空
unique: true // 唯一约束
},
email: {
type: DataTypes.STRING(100),
allowNull: false,
validate: {
isEmail: true // 校验邮箱格式
}
}
});
// 同步模型到数据库(创建表,如果表存在则更新结构)
await User.sync({ alter: true });
module.exports = User;
然后在service/user.js
里用模型查数据:
const User = require('../model/User');
exports.getUserList = async () => {
return await User.findAll({
attributes: ['id', 'name', 'email'], // 只返回这三个字段
limit: 10, // 分页:取前10条
offset: 0 // 跳过0条(第一页)
});
};
2. 原生SQL:复杂查询的救星
如果需要写复杂的联合查询(比如JOIN
多张表),ORM的语法会很绕——这时候用原生SQL更高效。比如用sequelize.query
执行原生查询:
const sequelize = require('../model/index');
exports.getUsersWithArticles = async () => {
const [rows] = await sequelize.query(`
SELECT u.id, u.name, a.title
FROM users u
LEFT JOIN articles a ON u.id = a.userId
WHERE u.id = :userId
`, {
replacements: { userId: 1 }, // 用replacements防止SQL注入
type: sequelize.QueryTypes.SELECT
});
return rows;
};
性能优化:让服务跑更快
Node.js是单线程的,但可以用集群模式和缓存提升性能。
1. 用PM2启动集群模式
PM2是Node.js的进程管理工具,可以启动多个进程,利用多核CPU:
# 安装PM2
npm install pm2 -g
# 启动app.js,进程数等于CPU核心数(-i max)
pm2 start src/app.js -i max
# 查看进程状态
pm2 list
PM2还能自动重启崩溃的进程、记录日志——生产环境必用。
2. 用Redis做数据缓存
频繁查询的数据(比如首页的热门文章),不要每次都查数据库——用Redis缓存10分钟:
// 安装redis客户端
npm install redis@4
// src/middleware/cache.js(Express版)
const redis = require('redis');
const client = redis.createClient({ url: process.env.REDIS_URL });
client.connect(); // 连接Redis
module.exports = async (req, res, next) => {
const cacheKey = `cache:${req.method}:${req.url}`; // 用请求方法+URL做缓存键
const cachedData = await client.get(cacheKey);
if (cachedData) {
// 有缓存,直接返回
return res.json(JSON.parse(cachedData));
}
// 没有缓存,执行后续逻辑,然后把结果存缓存
res.sendResponse = res.json;
res.json = async (data) => {
await client.setEx(cacheKey, 600, JSON.stringify(data)); // 缓存10分钟(600秒)
res.sendResponse(data);
};
next();
};
// 在路由里使用缓存中间件
router.get('/api/articles/hot', cacheMiddleware, articleController.getHotArticles);
安全:不能忽略的细节
后端服务的安全,重点防跨域、SQL注入、XSS攻击。
1. 跨域处理:用CORS中间件
浏览器的同源策略会阻止前端访问不同域名的接口——用cors
中间件解决:
// Express版
const cors = require('cors');
app.use(cors({
origin: process.env.CORS_ORIGIN, // 允许的前端域名(比如https://your-frontend.com)
credentials: true // 允许携带Cookie
}));
// Koa2版
const cors = require('@koa/cors');
app.use(cors({
origin: process.env.CORS_ORIGIN,
credentials: true
}));
2. 参数校验:防止恶意输入
用Joi
或Zod
校验请求参数,避免SQL注入和非法数据:
// 安装Joi
npm install joi
// src/validation/user.js
const Joi = require('joi');
exports.createUserSchema = Joi.object({
name: Joi.string().min(2).max(30).required(), // 名字至少2字符,最多30字符
email: Joi.string().email().required(), // 必须是邮箱格式
password: Joi.string().min(6).pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*d)/).required()
// 密码至少6字符,包含大小写字母和数字
});
// 在controller里使用校验
const { createUserSchema } = require('../validation/user');
exports.createUser = async (req, res) => {
const { error } = createUserSchema.validate(req.body);
if (error) {
return res.status(400).json({ code: 400, message: error.details[0].message });
}
// 后续业务逻辑
};
最后:写能跑的测试用例
后端服务不能“写完就上线”——至少要写接口测试。用Jest
+supertest
测试接口:
// tests/api/user.test.js
const request = require('supertest');
const app = require('../../src/app');
test('GET /api/users 应该返回用户列表', async () => {
const response = await request(app).get('/api/users');
expect(response.statusCode).toBe(200);
expect(response.body.data).toBeInstanceOf(Array);
});
test('POST /api/users 应该创建用户', async () => {
const response = await request(app)
.post('/api/users')
.send({ name: 'Bob', email: 'bob@example.com', password: 'Bob123!' });
expect(response.statusCode).toBe(201);
expect(response.body.data.name).toBe('Bob');
});
运行测试:
npx jest tests/api/user.test.js
原创文章,作者:,如若转载,请注明出处:https://zube.cn/archives/185