Skip to content

第6章 后端开发基础

作者

谭策 — 独立开发者 | AIOps 领域探索者

IT Online 微信公众号

许可证

MPL-2.0 © 谭策

本章导读

前面我们理解了项目的架构,现在进入实战阶段。本章将从零开始编写后端代码,带你完整走一遍:创建路由、编写中间件、开发服务、操作数据库、处理错误。

本章定位:这是你第一次真正动手写后端代码。我们会按照项目的实际编码风格来写,让你写出的代码能直接融入项目。

学习目标

阅读完本章后,你将能够:

  • 独立创建新的路由模块并注册到应用
  • 编写符合项目规范的中间件
  • 开发服务层的业务逻辑
  • 使用 better-sqlite3 进行增删改查操作
  • 实现 JWT 认证和权限控制
  • 编写统一的错误处理
  • 使用项目日志工具记录操作

6.1 开发环境准备

6.1.1 启动开发服务器

bash
# 进入后端目录
cd backend

# 安装依赖(首次)
npm install

# 启动开发模式(带热重载)
npm run dev

看到以下输出表示启动成功:

🚀 ITOps Agent Platform Backend running on 0.0.0.0:3000
📡 WebSocket server ready
🌍 Environment: development

6.1.2 验证接口可用

bash
# 健康检查(不需要认证)
curl http://localhost:3001/health/live

# 返回:
# {"status":"alive","timestamp":"2026-05-27T10:00:00.000Z"}

6.2 第一个路由模块

6.2.1 路由文件结构

在项目中创建一个新路由模块非常简单。以创建一个 notes(备忘录) 功能为例:

步骤 1:创建路由文件

bash
touch backend/src/routes/noteRoutes.ts

步骤 2:编写路由代码

typescript
import { Router, Request, Response } from 'express';
import { db } from '../models/database';
import { randomUUID } from 'crypto';

const router = Router();

// GET /api/notes - 获取所有备忘录
router.get('/', (_req: Request, res: Response) => {
  const notes = db.prepare('SELECT * FROM notes ORDER BY created_at DESC').all();
  res.json({
    success: true,
    data: notes
  });
});

// GET /api/notes/:id - 获取单个备忘录
router.get('/:id', (req: Request, res: Response) => {
  const { id } = req.params;
  const note = db.prepare('SELECT * FROM notes WHERE id = ?').get(id);

  if (!note) {
    return res.status(404).json({
      success: false,
      message: '备忘录不存在'
    });
  }

  res.json({
    success: true,
    data: note
  });
});

// POST /api/notes - 创建备忘录
router.post('/', (req: Request, res: Response) => {
  const { title, content } = req.body;

  if (!title) {
    return res.status(400).json({
      success: false,
      message: '标题不能为空'
    });
  }

  const id = randomUUID();
  const result = db.prepare(
    'INSERT INTO notes (id, title, content, created_at, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)'
  ).run(id, title, content || '');

  res.status(201).json({
    success: true,
    data: {
      id,
      title,
      content: content || ''
    }
  });
});

// PUT /api/notes/:id - 更新备忘录
router.put('/:id', (req: Request, res: Response) => {
  const { id } = req.params;
  const { title, content } = req.body;

  const existing = db.prepare('SELECT * FROM notes WHERE id = ?').get(id);
  if (!existing) {
    return res.status(404).json({
      success: false,
      message: '备忘录不存在'
    });
  }

  db.prepare(
    'UPDATE notes SET title = ?, content = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?'
  ).run(title || existing.title, content !== undefined ? content : existing.content, id);

  const updated = db.prepare('SELECT * FROM notes WHERE id = ?').get(id);
  res.json({
    success: true,
    data: updated
  });
});

// DELETE /api/notes/:id - 删除备忘录
router.delete('/:id', (req: Request, res: Response) => {
  const { id } = req.params;

  const existing = db.prepare('SELECT * FROM notes WHERE id = ?').get(id);
  if (!existing) {
    return res.status(404).json({
      success: false,
      message: '备忘录不存在'
    });
  }

  db.prepare('DELETE FROM notes WHERE id = ?').run(id);

  res.json({
    success: true,
    message: '备忘录已删除'
  });
});

export default router;

步骤 3:注册路由到应用

app.ts 中添加一行:

typescript
import noteRoutes from './routes/noteRoutes';

// 在受保护路由区域添加
app.use('/api/notes', rateLimiter, noteRoutes);

步骤 4:创建数据表

database.ts 中添加表创建语句:

typescript
db.exec(`
  CREATE TABLE IF NOT EXISTS notes (
    id TEXT PRIMARY KEY,
    title TEXT NOT NULL,
    content TEXT DEFAULT '',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  )
`);

6.2.2 测试路由

bash
# 创建备忘录
curl -X POST http://localhost:3001/api/notes \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -d '{"title":"测试备忘录","content":"这是一条测试内容"}'

# 获取所有备忘录
curl http://localhost:3001/api/notes \
  -H "Authorization: Bearer YOUR_TOKEN"

# 获取单个备忘录
curl http://localhost:3001/api/notes/你的备忘录ID \
  -H "Authorization: Bearer YOUR_TOKEN"

# 更新备忘录
curl -X PUT http://localhost:3001/api/notes/你的备忘录ID \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -d '{"title":"更新后的标题"}'

# 删除备忘录
curl -X DELETE http://localhost:3001/api/notes/你的备忘录ID \
  -H "Authorization: Bearer YOUR_TOKEN"

6.3 中间件开发

6.3.1 中间件的标准写法

中间件是 Express 中最重要的机制之一。项目中的中间件遵循统一模式:

typescript
import { Request, Response, NextFunction } from 'express';

// 标准中间件签名
export function myMiddleware(req: Request, res: Response, next: NextFunction) {
  // 1. 执行逻辑
  // 2. 可以选择返回响应(终止请求链)
  // 3. 或调用 next() 继续下一个中间件/路由
  next();
}

6.3.2 项目中的认证中间件详解

让我们逐行分析项目的 JWT 认证中间件:

typescript
// backend/src/middleware/auth.ts
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
import db from '../models/database';
import { env } from '../utils/env';
import { tokenBlacklist } from '../services/tokenBlacklist';

// 定义认证后的用户类型
interface AuthUser {
  id: string;
  username: string;
  email: string | null;
  role: string;
  enabled: number;
}

// 用户缓存(减少数据库查询)
const userCache = new Map<string, { user: AuthUser; expiresAt: number }>();
const USER_CACHE_TTL = 60 * 1000;  // 缓存 60 秒

export function authenticateToken(
  req: Request & { user?: AuthUser },
  res: Response,
  next: NextFunction
) {
  // 第1步:从请求头提取 Token
  const authHeader = req.headers.authorization;

  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({
      success: false,
      message: '未提供认证token'
    });
  }

  const token = authHeader.substring(7);

  // 第2步:检查 Token 是否在黑名单中
  if (tokenBlacklist.isBlacklisted(token)) {
    return res.status(401).json({
      success: false,
      message: 'Token已失效'
    });
  }

  // 第3步:验证 JWT Token
  try {
    const decoded = jwt.verify(token, env.JWT_SECRET, {
      algorithms: ['HS256']
    }) as jwt.JwtPayload & { id: string };

    // 第4步:从缓存或数据库获取用户信息
    let user: AuthUser | null = getCachedUser(decoded.id);
    if (!user) {
      const dbUser = db.prepare(
        'SELECT id, username, email, role, enabled FROM users WHERE id = ?'
      ).get(decoded.id) as AuthUser | undefined;

      if (dbUser) {
        user = dbUser;
        setCachedUser(decoded.id, user);
      }
    }

    // 第5步:检查用户是否存在且启用
    if (!user) {
      return res.status(401).json({ success: false, message: '用户不存在' });
    }

    if (!user.enabled) {
      return res.status(403).json({ success: false, message: '账户已被禁用' });
    }

    // 第6步:将用户信息挂载到请求对象,继续执行
    req.user = user;
    next();
  } catch (error) {
    if (error instanceof jwt.TokenExpiredError) {
      return res.status(401).json({ success: false, message: 'Token已过期' });
    }
    return res.status(401).json({ success: false, message: '无效的token' });
  }
}

认证中间件的执行流程

请求: GET /api/servers
Authorization: Bearer eyJhbGci...

1. 检查 Authorization 头
  ↓ 有 → 提取 Token
  ↓ 无 → 返回 401

2. 检查 Token 黑名单
  ↓ 不在黑名单 → 继续
  ↓ 在黑名单 → 返回 401

3. 验证 JWT 签名
  ↓ 有效 → 获取用户 ID
  ↓ 无效 → 返回 401

4. 查询用户信息(缓存或数据库)
  ↓ 找到用户 → 继续
  ↓ 未找到 → 返回 401

5. 检查用户状态
  ↓ enabled=1 → 继续
  ↓ enabled=0 → 返回 403

6. req.user = 用户信息

next() → 路由处理函数

6.3.3 角色权限中间件

项目提供了基于角色的权限控制:

typescript
export function requireRole(...allowedRoles: string[]) {
  return (req: Request & { user?: AuthUser }, res: Response, next: NextFunction) => {
    if (!req.user) {
      return res.status(401).json({
        success: false,
        message: '未认证'
      });
    }

    if (!allowedRoles.includes(req.user.role)) {
      return res.status(403).json({
        success: false,
        message: '权限不足'
      });
    }

    next();
  };
}

使用方式

typescript
// 只有管理员可以删除用户
app.delete('/api/users/:id',
  authenticateToken,
  requireRole('admin'),
  userDeleteHandler
);

// 管理员和普通用户都可以查看
app.get('/api/users',
  authenticateToken,
  requireRole('admin', 'user'),
  userListHandler
);

6.3.4 请求追踪中间件

项目为每个请求生成唯一的追踪 ID,方便日志追踪:

typescript
// backend/src/middleware/trace.ts
import { Request, Response, NextFunction } from 'express';
import { randomUUID } from 'crypto';

export function traceMiddleware(req: Request, res: Response, next: NextFunction) {
  // 为每个请求生成唯一 ID
  const traceId = randomUUID();
  (req as Request & { traceId: string }).traceId = traceId;

  // 在响应头中返回,方便前端调试
  res.setHeader('X-Trace-Id', traceId);

  next();
}

为什么需要请求追踪?

用户报告:"系统报错了!"

没有追踪 ID:
  日志: "错误: 数据库连接失败"
  日志: "错误: 查询超时"
  日志: "错误: 用户不存在"
  你: 不知道这些错误是不是同一个请求产生的

有追踪 ID:
  日志: "[abc-123] 请求开始: GET /api/servers"
  日志: "[abc-123] 数据库查询: SELECT * FROM servers"
  日志: "[abc-123] 错误: 查询超时"
  你: 精确知道是 /api/servers 请求的数据库查询超时了

6.4 服务层开发

6.4.1 服务的设计原则

服务层是业务逻辑的核心。项目中的服务遵循以下设计原则:

  1. 单一职责:一个服务只负责一个业务域
  2. 无状态:服务方法不持有请求级别的状态
  3. 可测试:每个方法都可以独立测试
  4. 依赖注入:通过参数或初始化注入依赖

6.4.2 登录服务实战

让我们通过实际的登录路由来理解后端开发模式:

typescript
// backend/src/routes/authRoutes.ts
import { Router, Request, Response } from 'express';
import { db } from '../models/database';
import bcrypt from 'bcryptjs';
import jwt, { SignOptions } from 'jsonwebtoken';
import { randomUUID } from 'crypto';
import { env } from '../utils/env';
import { logger } from '../utils/logger';

const router = Router();

// POST /api/auth/login
router.post('/login', async (req: Request, res: Response) => {
  try {
    const { username, password } = req.body;

    // 1. 查询用户
    const user = db.prepare(
      'SELECT * FROM users WHERE username = ?'
    ).get(username) as UserRecord | undefined;

    if (!user) {
      return res.status(401).json({
        success: false,
        message: '用户名或密码错误'
      });
    }

    // 2. 检查用户状态
    if (!user.enabled) {
      return res.status(403).json({
        success: false,
        message: '账户已被禁用'
      });
    }

    // 3. 验证密码
    const validPassword = await bcrypt.compare(password, user.password);
    if (!validPassword) {
      return res.status(401).json({
        success: false,
        message: '用户名或密码错误'
      });
    }

    // 4. 生成 JWT Token
    const accessToken = jwt.sign(
      {
        id: user.id,
        username: user.username,
        role: user.role,
        email: user.email
      },
      env.JWT_SECRET,
      { expiresIn: env.JWT_EXPIRES_IN } as SignOptions
    );

    const refreshToken = jwt.sign(
      { id: user.id, type: 'refresh' },
      env.JWT_SECRET,
      { expiresIn: '7d' } as SignOptions
    );

    // 5. 更新登录时间
    db.prepare(
      'UPDATE users SET updated_at = CURRENT_TIMESTAMP WHERE id = ?'
    ).run(user.id);

    // 6. 记录审计日志
    db.prepare(`
      INSERT INTO audit_logs (id, user_id, action, resource_type, 
        resource_id, details, ip_address, created_at)
      VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
    `).run(
      randomUUID(),
      user.id,
      'login',
      'auth',
      'login',
      JSON.stringify({ username }),
      req.ip
    );

    // 7. 返回结果
    res.json({
      success: true,
      message: '登录成功',
      data: {
        token: accessToken,
        refreshToken,
        user: {
          id: user.id,
          username: user.username,
          email: user.email,
          role: user.role
        }
      }
    });
  } catch (error) {
    logger.error('登录失败', error);
    res.status(500).json({
      success: false,
      message: '服务器错误'
    });
  }
});

6.4.3 登录流程图解

POST /api/auth/login
{ "username": "admin", "password": "password123" }

1. SELECT * FROM users WHERE username = 'admin'

   用户存在?
   ├─ 否 → 401 "用户名或密码错误"
   └─ 是 → 继续

2. 检查 user.enabled

   启用?
   ├─ 否 → 403 "账户已被禁用"
   └─ 是 → 继续

3. bcrypt.compare('password123', '$2a$12$...')

   密码正确?
   ├─ 否 → 401 "用户名或密码错误"(不暴露具体原因)
   └─ 是 → 继续

4. jwt.sign({ id, username, role, email }, JWT_SECRET, { expiresIn: '24h' })

5. UPDATE users SET updated_at = CURRENT_TIMESTAMP WHERE id = ?

6. INSERT INTO audit_logs ... (记录登录行为)

7. 返回 JSON:
   {
     "success": true,
     "data": {
       "token": "eyJhbGci...",
       "refreshToken": "eyJhbGci...",
       "user": { "id": "...", "username": "admin", ... }
     }
   }

6.4.4 服务层重构

当业务逻辑变得复杂时,应该将逻辑从路由中提取到服务层:

重构前(逻辑在路由中)

typescript
// 路由文件中有太多业务逻辑
router.post('/login', async (req, res) => {
  const { username, password } = req.body;
  const user = db.prepare('...').get(username);
  // ... 30+ 行业务逻辑
});

重构后(逻辑在服务层)

typescript
// backend/src/services/authService.ts
export const authService = {
  async login(username: string, password: string, ip?: string) {
    const user = db.prepare(
      'SELECT * FROM users WHERE username = ?'
    ).get(username) as UserRecord | undefined;

    if (!user || !user.enabled) {
      throw new Error('用户名或密码错误');
    }

    const validPassword = await bcrypt.compare(password, user.password);
    if (!validPassword) {
      throw new Error('用户名或密码错误');
    }

    const accessToken = jwt.sign({ ... }, env.JWT_SECRET, { expiresIn: '24h' });
    const refreshToken = jwt.sign({ ... }, env.JWT_SECRET, { expiresIn: '7d' });

    db.prepare('UPDATE users SET updated_at = CURRENT_TIMESTAMP WHERE id = ?')
      .run(user.id);

    db.prepare('INSERT INTO audit_logs ...').run(...);

    return {
      token: accessToken,
      refreshToken,
      user: { id: user.id, username: user.username, ... }
    };
  }
};

// 路由文件变得很简洁
router.post('/login', async (req, res) => {
  try {
    const { username, password } = req.body;
    const result = await authService.login(username, password, req.ip);
    res.json({ success: true, data: result });
  } catch (error) {
    if (error.message === '用户名或密码错误') {
      return res.status(401).json({ success: false, message: error.message });
    }
    res.status(500).json({ success: false, message: '服务器错误' });
  }
});

6.5 数据库操作详解

6.5.1 better-sqlite3 基础操作

项目使用 better-sqlite3,这是一个同步的 SQLite 驱动:

typescript
import Database from 'better-sqlite3';

// 打开数据库
const db = new Database('./data/itops.db');

// 查询单行
const user = db.prepare('SELECT * FROM users WHERE id = ?').get(1);

// 查询多行
const servers = db.prepare('SELECT * FROM servers').all();

// 插入数据
const result = db.prepare(
  'INSERT INTO servers (id, name, host, port) VALUES (?, ?, ?, ?)'
).run('uuid-1', 'prod-server', '192.168.1.1', 22);

console.log(result.lastInsertRowid);  // 插入行的 ID
console.log(result.changes);          // 受影响的行数

// 更新数据
db.prepare('UPDATE servers SET status = ? WHERE id = ?').run('online', 'uuid-1');

// 删除数据
db.prepare('DELETE FROM servers WHERE id = ?').run('uuid-1');

6.5.2 参数化查询(防 SQL 注入)

永远不要拼接 SQL 字符串!

typescript
// 错误做法(SQL 注入风险)
const user = db.prepare(
  `SELECT * FROM users WHERE username = '${username}'`
).get();

// 正确做法(参数化查询)
const user = db.prepare(
  'SELECT * FROM users WHERE username = ?'
).get(username);

// 多个参数
const result = db.prepare(
  'SELECT * FROM tasks WHERE user_id = ? AND status = ? ORDER BY created_at DESC'
).all(userId, status);

// 命名参数(可读性更好)
const result = db.prepare(
  'SELECT * FROM tasks WHERE user_id = $userId AND status = $status'
).all({ $userId: userId, $status: status });

6.5.3 事务处理

当需要执行多个数据库操作,且要么全部成功、要么全部失败时,使用事务:

typescript
// 创建事务
const createServer = db.transaction((data: CreateServerData) => {
  // 1. 插入服务器记录
  const serverResult = db.prepare(
    'INSERT INTO servers (id, name, host, port, created_at) VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)'
  ).run(data.id, data.name, data.host, data.port);

  // 2. 记录审计日志
  db.prepare(
    'INSERT INTO audit_logs (id, user_id, action, resource_type, resource_id, details, created_at) VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)'
  ).run(randomUUID(), data.userId, 'create', 'server', data.id, JSON.stringify(data), new Date().toISOString());

  // 如果任何一步失败,整个事务回滚
  return serverResult.lastInsertRowid;
});

// 执行事务
try {
  const id = createServer.run({
    id: randomUUID(),
    name: 'prod-server',
    host: '192.168.1.1',
    port: 22,
    userId: 'admin-id'
  });
  console.log('服务器创建成功,ID:', id);
} catch (error) {
  console.error('服务器创建失败,事务已回滚');
}

6.5.4 数据库初始化

项目启动时会自动创建所有表:

typescript
// backend/src/models/database.ts 模式
import Database from 'better-sqlite3';

const db = new Database('./data/itops.db');

// 启用 WAL 模式(更好的并发性能)
db.pragma('journal_mode = WAL');

// 开启外键约束
db.pragma('foreign_keys = ON');

// 创建用户表
db.exec(`
  CREATE TABLE IF NOT EXISTS users (
    id TEXT PRIMARY KEY,
    username TEXT UNIQUE NOT NULL,
    password TEXT NOT NULL,
    email TEXT,
    role TEXT DEFAULT 'user',
    enabled INTEGER DEFAULT 1,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  )
`);

// 创建服务器表
db.exec(`
  CREATE TABLE IF NOT EXISTS servers (
    id TEXT PRIMARY KEY,
    name TEXT NOT NULL,
    host TEXT NOT NULL,
    port INTEGER DEFAULT 22,
    username TEXT,
    password TEXT,
    status TEXT DEFAULT 'offline',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  )
`);

// ... 更多表创建

export default db;

6.6 日志工具使用

6.6.1 项目日志工具

项目使用自定义的日志工具,支持不同级别的日志输出:

typescript
import { logger } from './utils/logger';

// 不同级别的日志
logger.info('服务器启动成功');
logger.warn('配置文件未找到,使用默认配置');
logger.error('数据库连接失败', error);
logger.debug('请求参数:', req.body);

// 带上下文的日志
logger.info('用户登录', {
  userId: '123',
  username: 'admin',
  ip: '192.168.1.100'
});

6.6.2 日志级别说明

级别用途示例
debug开发调试信息请求参数、中间件执行
info正常业务流程服务器启动、用户登录
warn警告信息配置缺失、降级处理
error错误信息数据库连接失败、API 调用异常

6.7 错误处理

6.7.1 统一错误处理

项目使用集中式的错误处理中间件:

typescript
// backend/src/middleware/errorHandler.ts
import { Request, Response, NextFunction } from 'express';
import { logger } from '../utils/logger';

// 404 处理
export const notFoundHandler = (req: Request, res: Response) => {
  res.status(404).json({
    success: false,
    error: `Route ${req.method} ${req.originalUrl} not found`
  });
};

// 全局错误处理
export const errorHandler = (
  err: Error,
  req: Request,
  res: Response,
  _next: NextFunction
) => {
  const statusCode = (err as any).statusCode || 500;
  const message = err.message || 'Internal Server Error';

  // 记录错误日志
  logger.error(`${req.method} ${req.originalUrl}`, {
    error: message,
    stack: err.stack,
    traceId: (req as any).traceId
  });

  // 返回统一格式的错误响应
  res.status(statusCode).json({
    success: false,
    error: message
  });
};

6.7.2 自定义错误类

项目中可以定义自定义错误类,携带状态码:

typescript
// backend/src/types/errors.ts
export class AppError extends Error {
  public statusCode: number;

  constructor(message: string, statusCode: number) {
    super(message);
    this.statusCode = statusCode;
    this.name = 'AppError';
  }
}

// 使用示例
throw new AppError('服务器不存在', 404);
throw new AppError('权限不足', 403);
throw new AppError('请求参数错误', 400);

6.8 环境变量管理

6.8.1 环境变量配置

项目通过 utils/env.ts 统一管理环境变量:

typescript
// backend/src/utils/env.ts
export const env = {
  NODE_ENV: process.env.NODE_ENV || 'development',
  PORT: parseInt(process.env.PORT || '3000', 10),
  JWT_SECRET: process.env.JWT_SECRET || 'default-secret',
  JWT_EXPIRES_IN: process.env.JWT_EXPIRES_IN || '24h',
  DOUBAO_API_KEY: process.env.DOUBAO_API_KEY || '',
  OPENAI_API_KEY: process.env.OPENAI_API_KEY || '',
  ALLOWED_ORIGINS: (process.env.ALLOWED_ORIGINS || '*').split(',').map(s => s.trim()),
};

6.8.2 环境变量使用

typescript
import { env } from './utils/env';

// 使用环境变量
const port = env.PORT;
const jwtSecret = env.JWT_SECRET;

// 条件判断
if (env.NODE_ENV === 'production') {
  // 生产环境逻辑
}

6.9 项目规范总结

6.9.1 路由编写规范

typescript
// 标准路由模板
import { Router, Request, Response } from 'express';
import { db } from '../models/database';

const router = Router();

// 使用 try-catch 包裹所有异步操作
router.get('/', async (req: Request, res: Response) => {
  try {
    // 1. 获取参数
    const { param1, param2 } = req.query;

    // 2. 参数验证
    if (!param1) {
      return res.status(400).json({
        success: false,
        message: '参数不能为空'
      });
    }

    // 3. 业务逻辑
    const result = db.prepare('...').all();

    // 4. 返回结果
    res.json({
      success: true,
      data: result
    });
  } catch (error) {
    // 5. 错误处理(交给全局错误处理中间件)
    res.status(500).json({
      success: false,
      message: '服务器错误'
    });
  }
});

export default router;

6.9.2 响应格式规范

项目统一使用以下 JSON 格式:

typescript
// 成功响应
{
  "success": true,
  "data": { ... },
  "message": "操作成功"
}

// 错误响应
{
  "success": false,
  "error": "错误描述",
  "message": "详细错误信息"
}

6.9.3 HTTP 状态码使用规范

状态码含义使用场景
200成功正常 GET、POST 响应
201已创建成功创建资源(POST)
204无内容成功删除(不需要返回内容)
400请求错误参数验证失败
401未认证Token 缺失或过期
403权限不足已认证但没有操作权限
404未找到资源不存在
500服务器错误内部异常

本章小结

本章涵盖了后端开发的完整流程:

  1. 路由开发:创建路由文件 → 编写路由处理函数 → 注册到应用
  2. 中间件开发:理解中间件机制,编写认证、权限、追踪等中间件
  3. 服务层开发:将业务逻辑从路由中提取到服务层,保持路由简洁
  4. 数据库操作:增删改查、参数化查询、事务处理
  5. 错误处理:统一格式、自定义错误类、集中处理
  6. 日志记录:不同级别的日志输出
  7. 编码规范:响应格式、HTTP 状态码、代码组织

核心原则:路由只管分发,服务只管逻辑,数据库只管存储。


本章练习

基础练习

  1. 创建备忘录 CRUD:按照本章示例,完整实现 notes 的增删改查功能,并使用 curl 测试每个接口

  2. 添加认证:为 notes 路由添加 authenticateToken 中间件,确保只有登录用户才能访问

  3. 添加审计日志:每次创建、更新、删除备忘录时,向 audit_logs 表插入一条审计记录

进阶练习

  1. 创建服务层:将 notes 路由的业务逻辑提取到 noteService.ts 中,实现路由和逻辑的分离

  2. 添加分页查询:为 GET /api/notes 添加 pagepageSize 参数,实现分页功能

  3. 编写自定义错误类:创建 NotFoundErrorValidationError 等自定义错误类,在路由中使用

思考题

  1. 项目中的 authenticateToken 中间件使用了用户缓存(Map),这种设计有什么优势和风险?在什么情况下缓存可能导致问题?

  2. 为什么项目选择同步的 better-sqlite3 而非异步的 node-sqlite3?如果后端需要同时处理 1000 个请求,会产生什么问题?

  3. 路由中直接使用 db.prepare().run() 和通过服务层调用,哪种方式更好?为什么?


延伸阅读


本章回顾:你已经掌握了后端开发的基础技能!在下一章中,我们将进入前端开发,学习如何使用 React 构建用户界面。

基于 MPL-2.0 许可证发布