Node.js后端服务开发实战指南:从项目搭建到性能优化

先搭一个能落地的项目结构

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

Node.js后端服务开发实战指南:从项目搭建到性能优化

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对象整合了reqres,比如ctx.params.id对应URL里的:idctx.body用来设置响应体——比Express的req.params+res.json更简洁。

设计可维护的RESTful API

RESTful API不是“必须用GET查、POST增”的教条,而是用HTTP协议的语义表达资源操作。我总结了3条能直接落地的规则:

  1. 用名词表示资源:比如/api/users(用户列表)、/api/articles(文章列表),不要用/api/getUsers这种动词开头的URL。
  2. 用HTTP方法表示操作
  3. GET:查询(比如GET /api/users获取所有用户,GET /api/users/1获取ID=1的用户)
  4. POST:新增(比如POST /api/users创建一个用户)
  5. PUT:全量更新(比如PUT /api/users/1更新用户ID=1的所有信息)
  6. PATCH:部分更新(比如PATCH /api/users/1只更新用户的邮箱)
  7. DELETE:删除(比如DELETE /api/users/1删除用户ID=1)
  8. 用状态码传递结果:别只用200和500!比如:
  9. 201 Created:新增成功(比如POST /api/users后返回)
  10. 400 Bad Request:参数错误(比如没传name字段)
  11. 401 Unauthorized:未登录(比如没传Token)
  12. 403 Forbidden:没有权限(比如普通用户想删管理员)
  13. 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. 参数校验:防止恶意输入

JoiZod校验请求参数,避免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

(0)

相关推荐