04 写第一个 Express 接口:建立后端骨架
04 写第一个 Express 接口:建立后端骨架
本章你会做出什么
上一章的服务器只有一个文件。本章会把它整理为配置、应用、启动、错误四个职责清晰的文件,并让所有成功响应都包含一致的格式和请求编号。
先理解三个概念
1. 路由和响应
后端路由表示“某种方法访问某条路径时执行什么代码”。例如 GET /health 返回服务是否正常。
2. 状态码和业务响应
HTTP 状态码是协议级结果:200 成功,400 参数错,401 未登录,403 无权限,404 不存在,500 服务器异常。业务响应再包含 code、message 和数据。
3. 中间件
中间件会在正式路由前后统一执行工作。express.json() 读取 JSON 请求体,cors() 允许开发中的前端访问后端,错误处理中间件集中返回错误格式。
本章最终目录变化
server/src/
app.ts
config.ts
errors.ts
index.ts
utils.ts
一步一步操作
第 1 步:安装配置读取依赖
npm install -w server dotenv
第 2 步:创建配置和工具文件
创建下面提供的 config.ts 与 utils.ts。环境变量将来用于切换数据库和保管 JWT 密钥。
第 3 步:创建应用并替换启动文件
将路由放在 app.ts,让 index.ts 只负责监听端口。这样第 14 章的测试可直接调用 createApp(),无需真的占用端口。
关键文件完整代码
server/src/config.ts
import 'dotenv/config'
export interface AppConfig {
port: number
dbDriver: 'memory' | 'mysql'
jwtSecret: string
}
export const config: AppConfig = {
port: Number(process.env.PORT ?? 3001),
dbDriver: process.env.DB_DRIVER === 'mysql' ? 'mysql' : 'memory',
jwtSecret: process.env.JWT_SECRET ?? 'servicedesk-development-secret'
}
server/src/errors.ts
export class AppError extends Error {
constructor(
public status: number,
message: string,
public code = status
) {
super(message)
}
}
server/src/utils.ts
import { randomUUID } from 'node:crypto'
export const requestId = () => `req_${randomUUID().replaceAll('-', '').slice(0, 16)}`
server/src/app.ts
import cors from 'cors'
import express, { type NextFunction, type Request, type Response } from 'express'
import type { AppConfig } from './config.js'
import { AppError } from './errors.js'
import { requestId } from './utils.js'
const success = <T>(response: Response, data: T, message = 'ok') =>
response.json({ code: 0, message, data, requestId: requestId() })
export const createApp = (config: AppConfig) => {
const app = express()
app.use(cors())
app.use(express.json())
app.get('/api/v1/health', (_request, response) => {
success(response, { status: 'up', database: config.dbDriver })
})
app.use((error: unknown, _request: Request, response: Response, _next: NextFunction) => {
const appError = error instanceof AppError ? error : new AppError(500, '服务暂时不可用')
response.status(appError.status).json({
code: appError.code,
message: appError.message,
data: null,
requestId: requestId()
})
})
return app
}
server/src/index.ts
import { createApp } from './app.js'
import { config } from './config.js'
const app = createApp(config)
app.listen(config.port, () => {
console.log(`ServiceDesk API running at http://localhost:${config.port}/api/v1`)
})
server/.env.example
PORT=3001
DB_DRIVER=memory
JWT_SECRET=replace-this-in-real-environment
.env.example 只展示需要填写的项目,不要将真实密钥提交到 Git。
启动并验证
重新启动服务:
npm run dev
另开终端请求:
Invoke-RestMethod http://localhost:3001/api/v1/health | ConvertTo-Json -Depth 3
预期结构如下,请求编号每次不同:
{
"code": 0,
"message": "ok",
"data": {
"status": "up",
"database": "memory"
},
"requestId": "req_..."
}
常见报错与原因
| 报错 | 原因 | 修正 |
|---|---|---|
| 修改代码后旧错误还在 | 老的开发服务未重新加载或有多个服务 | 停止所有服务后重新运行 npm run dev |
dotenv/config 找不到 |
没有安装依赖 | 运行 npm install -w server dotenv |
| 浏览器显示 CORS 错误 | app.use(cors()) 遗漏或后端未启动 |
检查 app.ts 和服务输出 |
本章完成清单
- 我把启动逻辑和应用路由拆分开了。
- 健康接口返回统一响应格式。
- 我知道
401与403的区别。 - 我创建了可供后续配置使用的
.env.example。
面试时这一章能怎么讲
后端以
/api/v1作为版本前缀,统一返回code/message/data/requestId结构;启动入口与 Express 应用分离,便于后续用 Supertest 对应用进行自动测试。