CHAPTER 08 · 全栈
后端、数据与端到端类型
这是你最熟悉的领域:HTTP 接口、数据库、认证、后台任务。本章不重复后端基础,而是讲清楚在 TS 全栈中哪些做法不一样:前后端共享类型、serverless 下的数据库连接、以及“一个函数就是一个接口”带来的新安全模型。
- 说出 TS 后端的三种组织形态,并为一个项目做选择。
- 用 Drizzle 定义表结构,并解释类型如何从数据库一路流到前端组件。
- 解释 serverless 环境下数据库连接的问题和常见解法。
- 把“认证”和“授权”分开,并在数据访问层落实授权检查。
- 为耗时任务(如 AI 摘要)设计不阻塞请求的处理方式。
开场:改了一个字段名
在一个 Java 后端 + 独立前端的项目里,后端把 summary 字段改名为 abstract。前端没有同步修改,上线后详情页显示空白,直到用户反馈才发现。通常的防御手段是接口文档、契约测试和联调。在 TS 全栈项目里,同样的修改能否在按下保存键的那一刻就暴露出来?
答案方向:可以。因为前后端是同一种语言、同一个仓库,类型可以通过 import 从数据库结构一路“流”到界面组件:表结构定义推导出行的类型,服务端函数的返回类型由查询推导,前端调用的结果类型又由服务端函数推导。改了源头,所有下游使用处会立即出现编译错误。这就是“端到端类型安全(end-to-end type safety)”。它不能替代运行时校验(第 04 章),但能消灭一大类“前后端不同步”的 bug。
TS 后端的三种形态
| 形态 | 是什么 | 例子 | 适合 |
|---|---|---|---|
| 元框架内置 | 后端代码写在全栈框架里:服务器组件、服务器函数、路由处理器 | Next.js Server Actions / Route Handlers;TanStack Start createServerFn | 只有一个 Web 前端的产品;最省事 |
| 独立 API 服务 | 一个单独的 HTTP 服务,前端通过 fetch 调用 | Hono、Fastify、Express、NestJS | 多客户端(Web + 移动 + 第三方);需要独立扩展和部署 |
| 类型化 RPC | 在独立服务之上,让客户端“像调函数一样”调用接口并获得类型 | tRPC、oRPC、Hono RPC 客户端 | 前后端都是 TS,希望独立部署又保留类型 |
对大多数个人项目和早期产品,从第一种开始。当出现第二个客户端(例如移动 App 或开放 API)时,再把业务逻辑抽到独立服务。为了让这次迁移容易,关键是:不要把业务逻辑写在框架特定的文件里,而是写在普通的 TS 模块中,框架层只做“适配”。
分层:和你熟悉的一样,只是更薄
和 Spring 的 Controller → Service → Repository 分层对应,但通常更薄:没有依赖注入容器,模块本身就是单例;没有 DTO 类,用类型推导和 Pick/Omit 表达“对外暴露哪些字段”。
Hono:一个到处都能跑的 API 框架 会用
如果需要独立的 API 服务,Hono 是 2026 年最通用的选择:基于 Web 标准的 Request/Response,同一份代码能部署到 Cloudflare Workers、Node、Bun、Deno、Vercel;体积小、速度快、类型推导好。风格上接近 Express 或 Go 的 Gin。
数据库与 ORM 会用
选数据库
- Postgres:默认选择。托管服务有 Neon、Supabase 等(都提供免费额度),自托管也很成熟。它还能通过 pgvector 扩展做向量检索(第 11 章)。
- SQLite:在 Cloudflare 上是 D1;也有 Turso(libSQL)。简单、便宜、单文件,适合“每个用户或租户一个库”的架构和中小型应用。
选 ORM:Drizzle 还是 Prisma
| Drizzle | Prisma | |
|---|---|---|
| 定义方式 | 用 TS 代码定义表 | 专用的 schema.prisma 文件,生成客户端 |
| 查询风格 | 接近 SQL(select/where/join),也有关系查询 API | 对象式 API,抽象程度更高 |
| 边缘 / serverless | 轻量,天然适配 Workers 和 D1 | 新版本已改进,但要看具体适配方式 |
| 什么时候选 | 懂 SQL、想要透明和轻量 | 团队习惯、需要成熟的工具链(Studio、迁移流程) |
本教程使用 Drizzle。作为后端开发者,你会发现它几乎就是“带类型的 SQL”:
迁移(migration):修改 schema.ts 后,运行 drizzle-kit generate 生成 SQL 迁移文件(要审查并提交),再用 drizzle-kit migrate 应用。让 Agent 改表结构时,要求它展示生成的 SQL——删列、改类型这类破坏性变更必须由你确认。
serverless 下的连接问题 原理
传统服务是一个常驻进程,启动时建立一个连接池,所有请求复用。Serverless 函数则可能同时存在成百上千个实例,每个实例各自建立连接——很快就会耗尽 Postgres 的最大连接数(通常只有几百个)。常见解法:
- 使用数据库服务商提供的连接池地址(如 PgBouncer 模式的 URL)。
- 使用基于 HTTP 或 WebSocket 的serverless 驱动(如 Neon 的 serverless driver),每次查询不需要长期占用 TCP 连接。
- 在 Cloudflare 上使用 Hyperdrive:它在边缘维护到你数据库的连接池并缓存查询。
- 在自托管的常驻进程里,就像你熟悉的那样,模块级别创建一个连接池即可。
同理,“在模块顶层建一个全局客户端”的写法,在常驻进程里是正确的单例,在某些 serverless 平台上却可能跨请求共享了不该共享的状态。这是第 09 章“运行时的形状”要讨论的话题。
认证与授权 原理
两个词经常被混用,但必须分开思考:
- 认证(authentication):你是谁?——登录、会话、OAuth。这部分用成熟的库,不要自己写。
- 授权(authorization):你能做这件事吗?——这条收藏属于你吗?你是管理员吗?这部分只能你自己写,因为它就是业务规则。
| 方案 | 类型 | 特点 |
|---|---|---|
| Better Auth | 开源库,自托管 | TS 原生;用户和会话存在你自己的数据库;插件支持 OAuth、2FA、组织等;可跑在各种平台 |
| Clerk | 托管服务 | 开箱即用的登录界面和用户管理;按用户量收费;数据在第三方 |
| Auth.js | 开源库 | 历史悠久,与 Next.js 集成深 |
| Supabase Auth | 托管(随数据库) | 与 Supabase 数据库的行级安全(RLS)结合 |
“不安全的直接对象引用(Insecure Direct Object Reference,IDOR)”:接口是 DELETE /links/:id,代码只检查了“用户已登录”,就按 id 删除。结果任何登录用户都能删除别人的收藏。防御方法就是上面 remove 函数的写法:把“属于当前用户”作为查询条件的一部分,而不是查出来之后再比较(后者容易被遗漏)。给 Agent 的每个涉及数据修改的任务,都应该明确写出授权规则。
校验与错误处理
和 Java 相比,TS 社区在错误处理上没有统一约定。实用的做法:
- 入口校验:所有外部输入在适配层用 Zod 解析,失败返回 400/422 和字段级错误信息。
- 预期内的业务失败用返回值表达:例如
{ ok: false, reason: "duplicate" }这样的判别联合,调用方被类型系统强制处理每种情况。 - 意外错误抛出异常,由框架的统一错误处理器记录日志并返回 500,不把堆栈暴露给客户端。
耗时任务:不要让用户等在请求上 知道
LinkNote 的“抓取网页 + LLM 生成摘要”可能需要 10~60 秒。如果放在保存请求里同步完成:用户盯着转圈;serverless 函数可能超时;中途失败还得整体重来。标准做法是异步化:
“工作流(workflow / durable execution)”是近年流行的升级版:把任务写成多个步骤,平台持久化每一步的结果,失败只重试失败的那一步,还能“等待几小时后继续”。对 AI 应用(多步调用、需要人工审批)特别合适。
其他常见的后端组件:对象存储(S3、Cloudflare R2、Vercel Blob)存文件,通常用预签名 URL 让浏览器直传;键值缓存(Redis、Upstash、Cloudflare KV)做缓存和限流;定时任务(cron)由平台提供。知道
remove(userId, id),删除条件同时包含 id 和 userId(防止 IDOR),返回是否删除成功。适配层:未登录 401,id 不是 UUID 返回 400,删除不到返回 404。为‘删除他人的收藏’写一个测试,预期 404。”本章词汇:端到端类型安全、单一事实来源、适配层 / 服务层、迁移、连接池、认证 vs 授权、IDOR、幂等(idempotent)、指数退避(exponential backoff)、异步任务 / 队列 / 持久化工作流。
- tRPC / 服务器函数和 REST 怎么选?只服务自己的 TS 前端时,类型化 RPC 开发效率最高;需要开放给第三方或多语言客户端时,REST(配 OpenAPI)更合适。
- serverless 连数据库有什么问题?实例多、生命周期短,容易耗尽连接;用连接池代理、HTTP 驱动或 Hyperdrive。
- 如何保证后台任务可以安全重试?设计成幂等:用业务 ID 去重、先检查状态再执行、写入使用 upsert。
自检
1. 你把 links.title 改成了 links.headline,重新运行类型检查。哪些地方会报错?哪些地方不会报错但仍然会出问题?
会报错:所有通过推导类型访问 .title 的服务层、服务器函数和组件代码。不会报错:数据库本身(需要迁移才会改列名);用 as 断言或 any 绕过类型的地方;手写的 SQL 字符串;外部系统(如缓存里的旧数据、第三方调用你 API 的客户端)。端到端类型只覆盖“同一次编译”内的代码。
2. Agent 写的服务层是:先 findById(id),然后 if (link.userId !== user.id) throw Forbidden,再删除。这样写安全吗?和把条件写进 where 相比有什么差别?
逻辑上是安全的,但有两个弱点:一是依赖每个开发者(和 Agent)记得写这个 if,容易在新接口里遗漏;二是“检查”和“删除”是两次操作,中间有时间窗口(对删除影响不大,对更新可能有竞态)。写进 where 让授权成为查询本身的一部分,更难被遗漏。返回 404 而不是 403 还能避免泄露“这个 id 存在”的信息。
3. 某个 serverless 应用在流量高峰时报 “too many connections”,低峰时一切正常。最可能的原因是什么?
高峰时平台启动了大量函数实例,每个实例都建立了自己的数据库连接(或连接池),总数超过了数据库上限。解决:换用连接池地址 / serverless 驱动 / Hyperdrive,并确认每个实例的连接池大小很小(如 1)。