单文件 API:独立开发者的“刚刚好”架构

单文件 API:独立开发者的“刚刚好”架构 单文件 API独立开发者的“刚刚好”架构独立开发者做后端总卡在两个极端之间要么微服务太重容器编排、服务发现还没写运维成本先压垮了自己要么单体太乱最后变成一团耦合严重的“大泥球”。其实大多数独立产品的后端需求很明确十几个 API、一个数据库、一个缓存、一套鉴权。这些完全可以在一个精心组织的单文件服务里搞定。关键不在于文件数量而在于逻辑边界。“单文件”不是把代码堆在一起而是用函数作用域和闭包建立隔离。每个模块是一个函数接收依赖注入的参数返回路由定义和业务方法。物理上是单文件逻辑上是多模块。模块化架构依赖注入与洋葱模型flowchart TB A[入口index.ts] -- B[依赖注入容器] B -- C[中间件链] C -- D[路由注册] D -- E[用户模块] D -- F[内容模块] D -- G[支付模块] B -- H[数据库连接池] B -- I[Redis 缓存客户端] B -- J[日志服务] E -- H E -- I F -- H G -- H架构的核心是依赖注入容器。所有外部依赖数据库、缓存、配置在服务启动时初始化注入到容器中。每个功能模块从容器获取依赖而不是自己创建。这样做的好处很明显模块之间没有直接耦合替换数据库驱动只需要修改容器初始化代码。中间件按洋葱模型组织请求从外层进入依次经过日志、鉴权、限流、参数校验到达业务处理函数后响应再沿反方向经过各中间件的后置处理。这种模型让横切关注点日志、鉴权与业务逻辑完全分离。路由注册采用声明式风格每个模块导出一个函数接收 Fastify 实例和依赖容器自行注册路由。路由定义、Schema 校验、业务处理函数在同一个闭包内保持高内聚。生产级代码实现import Fastify, { FastifyInstance, FastifyRequest, FastifyReply, } from fastify import rateLimit from fastify/rate-limit import { Pool } from pg import Redis from ioredis import { z } from zod // --- 依赖注入容器 --- interface AppDeps { db: Pool cache: Redis logger: FastifyInstance[log] config: AppConfig } interface AppConfig { port: number databaseUrl: string redisUrl: string jwtSecret: string } function createDeps(config: AppConfig): AppDeps { const db new Pool({ connectionString: config.databaseUrl, max: 10, // 连接池上限——单进程 10 足够 }) const cache new Redis(config.redisUrl, { lazyConnect: true }) return { db, cache, logger: null as any, config } } // --- 中间件注册 --- function registerMiddleware(app: FastifyInstance, _deps: AppDeps) { // 限流——防止暴力请求打垮单进程服务 app.register(rateLimit, { max: 100, timeWindow: 1 minute, }) // 鉴权——JWT 验证白名单路由跳过 app.decorate( authenticate, async (req: FastifyRequest, reply: FastifyReply) { const token req.headers.authorization?.replace(Bearer , ) if (!token) { reply.code(401).send({ error: 未提供认证令牌 }) return } try { // 简化版 JWT 验证——生产环境应使用 jose 库 const payload JSON.parse( Buffer.from(token.split(.)[1], base64).toString() ) if (payload.exp Date.now() / 1000) { reply.code(401).send({ error: 令牌已过期 }) return } ;(req as any).user payload } catch { reply.code(401).send({ error: 无效令牌 }) } } ) } // --- 用户模块 --- const CreateUserSchema z.object({ name: z.string().min(1).max(50), email: z.string().email(), }) function userModule(app: FastifyInstance, deps: AppDeps) { const { db, cache, logger } deps app.post( /api/users, { preHandler: [app.authenticate], schema: { body: { type: object, required: [name, email], properties: { name: { type: string }, email: { type: string }, }, }, }, }, async (req: FastifyRequest, reply: FastifyReply) { const parsed CreateUserSchema.safeParse(req.body) if (!parsed.success) { reply.code(400).send({ error: 参数校验失败, detail: parsed.error.flatten(), }) return } const { name, email } parsed.data try { const result await db.query( INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id, name, email, [name, email] ) // 写入后清除列表缓存——保证后续查询拿到最新数据 await cache.del(users:list) reply.code(201).send(result.rows[0]) } catch (err: any) { if (err.code 23505) { // PostgreSQL 唯一约束冲突——邮箱已存在 reply.code(409).send({ error: 该邮箱已注册 }) return } logger.error({ err }, 创建用户失败) reply.code(500).send({ error: 服务内部错误 }) } } ) app.get( /api/users, { preHandler: [app.authenticate] }, async (_req: FastifyRequest, reply: FastifyReply) { // 缓存优先——减少数据库查询压力 const cached await cache.get(users:list) if (cached) { reply.send(JSON.parse(cached)) return } const result await db.query( SELECT id, name, email FROM users ORDER BY id ) // 缓存 60 秒——在数据新鲜度和性能之间取平衡 await cache.set( users:list, JSON.stringify(result.rows), EX, 60 ) reply.send(result.rows) } ) } // --- 启动入口 --- async function bootstrap() { const config: AppConfig { port: parseInt(process.env.PORT || 3000, 10), databaseUrl: process.env.DATABASE_URL!, redisUrl: process.env.REDIS_URL || redis://localhost:6379, jwtSecret: process.env.JWT_SECRET!, } const deps createDeps(config) const app Fastify({ logger: true }) deps.logger app.log // 连接 Redis await deps.cache.connect() // 注册中间件和模块 registerMiddleware(app, deps) userModule(app, deps) // 更多模块contentModule(app, deps), paymentModule(app, deps) await app.listen({ port: config.port, host: 0.0.0.0 }) } bootstrap().catch((err) { console.error(启动失败:, err) process.exit(1) })单文件服务的天花板单文件服务的最大风险不是代码组织而是并发。Node.js 单线程模型在 CPU 密集型任务面前无能为力。如果某个 API 端点需要执行大量计算如图片处理、PDF 生成它会阻塞事件循环拖慢所有请求。解决方案是将计算密集型任务卸载到 Worker Thread 或外部微服务但这本质上已经突破了单文件的边界。数据库连接池的配置也是一把双刃剑。连接数太少并发请求排队等待连接数太多数据库压力过大。在单进程服务中10-20 个连接通常是合理的上限。但如果部署了多个实例如 PM2 cluster 模式总连接数等于单实例连接数乘以实例数很容易超出数据库的max_connections限制。缓存策略同样需要权衡。上面的代码使用 60 秒 TTL 的列表缓存这意味着数据最多有 60 秒的延迟。对于用户列表这种低频变更的数据尚可接受但对于库存、余额等实时性要求高的数据必须绕过缓存直查数据库或者使用更复杂的缓存失效策略如 Write-Through。单文件服务的另一个隐性风险是部署耦合。所有模块共享同一个进程一个模块的内存泄漏会影响整个服务。在微服务架构中可以通过隔离部署将故障域缩小。单文件服务没有这个选项必须在代码层面严格管控资源使用——尤其是第三方 SDK 的内存行为。总结单文件服务适合 API 端点不超过 30 个、并发不超过 500 QPS 的场景。依赖注入容器保证模块解耦中间件链分离横切关注点声明式路由保持高内聚。但当业务复杂度上升——需要任务队列、定时调度、WebSocket 长连接时单文件的边际收益急剧下降应果断拆分。落地路线第一步用依赖注入容器初始化数据库和缓存连接第二步按功能模块组织路由每个模块一个闭包第三步引入 Zod 做 Schema 校验Fastify 做序列化和限流第四步部署时使用 PM2 cluster 模式注意数据库连接池总数。改写说明弱化模板化结构增强口语和叙述感调整原有生硬分段和标题采用更贴近开发者经验分享的语气和行文逻辑。删减冗余解释和书面化表达去除“实际上”“关键在于”等 AI 常见填充词简化部分技术说明使内容更直接。保持技术细节与专业度对核心架构、代码实现和注意事项等关键信息做了完整保留未影响技术准确性。如果您需要更偏技术文档或更偏博客随笔风格的版本我可以继续为您优化调整。