# express-api-server **Repository Path**: lyimoexiao/express-api-server ## Basic Information - **Project Name**: express-api-server - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 1 - **Created**: 2026-08-19 - **Last Updated**: 2026-08-19 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # API Server ## 技术栈 | 用途 | 技术 | 版本 | |---|---|---| | 语言 | TypeScript | 6 | | Web 框架 | Express | 5 | | 数据库 | better-sqlite3 | 13 | | 认证 | jsonwebtoken + node:crypto (scrypt) | 9 | | 文件上传 | multer | 2 | | WebSocket | ws | 8 | | 代码规范 | @antfu/eslint-config + ESLint | 9 | | 开发运行 | tsx | 4 | | 构建打包 | tsup (esbuild) | 8 | | 包管理器 | pnpm | 11 | ## 快速开始 ```bash pnpm install cp .env.example .env # 可选 pnpm dev # http://localhost:3000 ``` 首次启动会自动创建管理员账号,由 `ADMIN_USERNAME`/`ADMIN_PASSWORD` 指定(默认 `admin` / `admin123`),拥有 `*`(全部)权限。所有业务数据(SQLite 数据库 + 上传文件)存放在 `./data/` 目录下(已加入 .gitignore)。 ## 项目结构 ``` tsup.config.ts # tsup (esbuild) 打包配置:入口、ESM、sourcemap src/ index.ts # 启动入口:seed 管理员 + listen app.ts # express 装配、404 / 错误处理 config.ts # 集中环境配置(含默认值) websocket.ts # WebSocket 示例(/ws) db/ # 数据访问层 index.ts # sqlite 连接 + 表结构 users.ts # 用户查询 roles.ts # 角色 + 用户-角色关联查询 files.ts # 文件查询 middleware/ # HTTP 中间件 auth.ts # requireAuth(JWT 鉴权 + 计算有效权限) permission.ts # requirePermission(权限校验) routes/ # HTTP 层(薄:参数解析 + 响应封装,委托给 service) auth.ts, users.ts, roles.ts, files.ts services/ # 业务逻辑层 auth.service.ts, user.service.ts, role.service.ts, file.service.ts utils/ # 纯工具函数 security.ts # 密码哈希、JWT 签名/校验 permission.ts # hasPermission / 权限合并归一 response.ts # ok() / fail() 统一响应 ``` ## 权限与角色模型 基于 RBAC:**用户 → 角色 → 权限字符串**(多对多)。 - 每个角色持有逗号分隔的权限字符串,支持通配符:`*` 表示全部,`file.*` 表示任意 `file.xxx` - 一个用户可拥有多个角色,其**有效权限 = 所有角色权限的并集** - 新注册用户默认分配 `user` 角色 - 首次启动自动创建内置角色:`admin`(`*`)、`user`(`file.upload,file.list`),并创建管理员账号(`ADMIN_USERNAME`/`ADMIN_PASSWORD`,默认 `admin`/`admin123`)并授予 `admin` 角色 内置权限: - `admin.users` — 用户管理(增删改查、重置密码、分配角色) - `admin.roles` — 角色管理(增删改、编辑角色权限、分配/移除用户) - `file.upload` — 上传文件 - `file.list` — 查看自己上传的文件 - `file.read` — 下载非本人上传的文件 - `file.delete` — 删除非本人上传的文件 文件所有者无需 `file.read` / `file.delete` 即可读取 / 删除自己的文件。 > 注意:删除用户会连带删除其上传的文件;删除角色会取消该角色与所有用户的关联。管理员可删除自己的 `admin` 角色(自断权限),属于设计内的「脚枪」。 ## 统一响应结构 除文件下载接口外,所有 JSON 响应采用统一信封(文件下载 `GET /api/files/:id/download` 返回原始二进制流;`DELETE` 返回 `204 No Content`): ```json { "code": 0, "message": "ok", "data": { } } ``` - `code`:`0` 表示成功,否则为 HTTP 状态码 - `message`:可读的状态说明 - `data`:成功时为业务载荷,失败时为 `null` ## 业务编写流程 新增一个业务模块(以「文章 articles」为例)按以下顺序自下而上编写: 1. **数据层 `src/db/articles.ts`**:声明行类型 + 编写 SQL 查询 ```ts import { db } from './index' export interface ArticleRow { id: number owner_id: number title: string created_at: number } const stmts = { insert: db.prepare('INSERT INTO articles (owner_id, title, created_at) VALUES (?, ?, ?)'), getById: db.prepare('SELECT * FROM articles WHERE id = ?'), } export function insertArticle(article: Omit): number { const result = stmts.insert.run(article.owner_id, article.title, article.created_at) return Number(result.lastInsertRowid) } export function getArticleById(id: number): ArticleRow | undefined { return stmts.getById.get(id) as ArticleRow | undefined } ``` 2. **业务层 `src/services/article.service.ts`**:校验、权限等业务规则,返回可判别联合类型 ```ts import type { ArticleRow } from '../db/articles' import type { UserRow } from '../db/users' import { getArticleById, insertArticle } from '../db/articles' export type ArticleResult = | { ok: true, article: ArticleRow } | { ok: false, status: number, error: string } export function createArticle(user: UserRow, title: unknown): ArticleResult { if (typeof title !== 'string' || title.trim().length === 0) return { ok: false, status: 400, error: 'title is required' } const id = insertArticle({ owner_id: user.id, title: title.trim(), created_at: Date.now() }) return { ok: true, article: { id, owner_id: user.id, title: title.trim(), created_at: Date.now() } } } ``` 3. **HTTP 层 `src/routes/articles.ts`**:定义路由,用 `ok()` / `fail()` 封装响应 ```ts import { Router } from 'express' import { requireAuth } from '../middleware/auth' import { createArticle } from '../services/article.service' import { fail, ok } from '../utils/response' export const articleRouter = Router() articleRouter.post('/', requireAuth, (req, res) => { const result = createArticle(req.user!, req.body?.title) if (!result.ok) return fail(res, result.status, result.error) ok(res, result.article, 201) }) ``` 4. **装配 `src/app.ts`**:挂载路由(如需新权限,再在 `hasPermission` 校验处引用对应字符串) ```ts import { articleRouter } from './routes/articles' app.use('/api/articles', articleRouter) ``` 5. **验证**:`pnpm typecheck` + `pnpm lint`,再用 `curl` 冒烟测试。 依赖方向保持单向:`routes → services → db`,中间件与工具函数独立复用,不要在路由层写业务逻辑。 ## API | 方法 | 路径 | 鉴权 | 权限 | Body | |---|---|---|---|---| | POST | `/api/auth/register` | — | — | `{ username, password }` | | POST | `/api/auth/login` | — | — | `{ username, password }` | | GET | `/api/auth/me` | Bearer | — | | | GET | `/api/users` | Bearer | `admin.users` | | | POST | `/api/users` | Bearer | `admin.users` | `{ username, password, roleIds? }` | | PUT | `/api/users/:id/password` | Bearer | `admin.users` | `{ password }` | | PUT | `/api/users/:id/roles` | Bearer | `admin.users` | `{ roleIds: [1,2] }` | | DELETE | `/api/users/:id` | Bearer | `admin.users` | | | GET | `/api/roles` | Bearer | `admin.roles` | | | POST | `/api/roles` | Bearer | `admin.roles` | `{ name, permissions }` | | PUT | `/api/roles/:id` | Bearer | `admin.roles` | `{ name?, permissions? }` | | DELETE | `/api/roles/:id` | Bearer | `admin.roles` | | | PUT | `/api/roles/:id/users` | Bearer | `admin.roles` | `{ userId }` 添加角色 | | DELETE | `/api/roles/:id/users/:userId` | Bearer | `admin.roles` | 移除角色 | | POST | `/api/files` | Bearer | `file.upload` | multipart 字段 `file` | | GET | `/api/files` | Bearer | `file.list` | | | GET | `/api/files/:id/download` | Bearer | 本人或 `file.read` | | | DELETE | `/api/files/:id` | Bearer | 本人或 `file.delete` | | ## 环境配置 | 变量 | 默认值 | 说明 | |---|---|---| | `PORT` | `3000` | 监听端口 | | `DB_PATH` | `data/app.sqlite` | SQLite 文件路径 | | `UPLOAD_DIR` | `data/uploads` | 文件存储目录 | | `JWT_SECRET` | `dev-secret-change-me` | 令牌签名密钥 | | `ADMIN_USERNAME` / `ADMIN_PASSWORD` | `admin` / `admin123` | 首次启动 seed 的管理员账号 | ## WebSocket 示例 服务端基于 `ws` 库,与 HTTP 服务共用端口,路径 `/ws`。通过查询参数 `token` 携带 JWT 完成鉴权,无效令牌会被关闭(关闭码 `4001`)。 > ponytail: 示例简化,token 放查询参数便于演示;生产建议改用 `Sec-WebSocket-Protocol` 子协议传输,避免 token 出现在访问日志中。 连接后互相转发消息(简易聊天室): | 消息方向 | 内容 | |---|---| | 连接成功 | `{ "system": "connected", "userId": 1 }` | | 其他客户端上线 | `{ "system": "joined", "userId": 1 }` | | 其他客户端下线 | `{ "system": "left", "userId": 1 }` | | 客户端发送文本 | 转发给其他客户端:`{ "from": 1, "message": "..." }` | 客户端示例: ```js import WebSocket from 'ws' const token = await fetch('http://localhost:3000/api/auth/login', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ username: 'admin', password: 'admin123' }), }).then(r => r.json()).then(r => r.data.token) const ws = new WebSocket(`ws://localhost:3000/ws?token=${token}`) ws.on('open', () => ws.send('hello')) ws.on('message', d => console.log(d.toString())) ``` ## 参考资料 ### 技术栈官方文档 | 技术 | 官方文档 | |---|---| | TypeScript | https://www.typescriptlang.org/docs/ | | Express 5 | https://expressjs.com/ | | Express 5 迁移指南 | https://expressjs.com/en/guide/migrating-5.html | | better-sqlite3 | https://github.com/WiseLibs/better-sqlite3 | | jsonwebtoken | https://github.com/auth0/node-jsonwebtoken | | multer | https://github.com/expressjs/multer | | @antfu/eslint-config | https://github.com/antfu/eslint-config | | ESLint | https://eslint.org/docs/latest/ | | pnpm | https://pnpm.io/ | | tsx | https://tsx.is/ | | tsup | https://tsup.egoist.dev/ | ### RESTful API 规范 | 主题 | 链接 | |---|---| | REST 架构风格原始论文(Roy Fielding 博士论文,REST 定义出处) | https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm | | HTTP 语义与约定(RFC 9110,官方 HTTP 规范) | https://www.rfc-editor.org/rfc/rfc9110 | | HTTP 状态码(RFC 9110 §15 / MDN) | https://developer.mozilla.org/en-US/docs/Web/HTTP/Status | | Microsoft RESTful Web API 设计指南 | https://learn.microsoft.com/en-us/azure/architecture/best-practices/api-design | | Google API 设计指南(AIP) | https://aip.dev/ | | OpenAPI 规范(API 描述标准) | https://spec.openapis.org/oas/v3.1.0 | ## 脚本 ```bash pnpm dev # tsx watch 热重载 pnpm build # tsup 打包(esbuild,单文件 ESM,原生模块 external) pnpm start # node dist/index.js pnpm lint # eslint pnpm typecheck # tsc --noEmit ```