成为全栈·Node 后端篇·后端工程从零搭建:TypeScript、目录与热更新
前置知识:建议先读 为什么前端工程师要走向全栈(M0-01)、七个子项目技术栈的定法(M0-03)。这篇是 M1 的第一站,后面三十篇都建立在这个地基上。
上一篇产品篇里,我把「一个真实的多端文章系统」摊开给你看:七个子项目、一份共享 API 契约、一条共同的版本历史。从这一篇起,我们要钻进其中一个子项目------Node 后端,把它从 git init 之后的一张白纸,搭成能跑、能测、能部署的工程。
我会用三十一篇讲完它。而所有故事,都从今天这个最朴素的问题开始:一个后端工程,初始化时到底该长什么样?

一、为什么后端也得用 TypeScript
很多前端同学第一次写后端,会本能地想:「反正也是写 JS,直接用 Node 跑 .js 文件不就行了?」我早期也这么干过,后来在生产环境踩够了坑,才认定一条底线:后端工程,能用 TypeScript 就别用裸 JavaScript。
原因不是「TS 更高级」,而是后端和前端的失败成本完全不同。
前端代码跑在用户浏览器里,出个类型错误,最坏是某块 UI 白屏,刷新一下也许就好了,影响面有限。但后端代码跑在服务器上,是所有人共享的那一个进程:一个 undefined 被当成字符串拼接进 SQL、一个本该是数字的 id 变成了 NaN 写进数据库、一个本该返回数组的接口返回了 null 被上游当成对象遍历------这些都不是「刷新就好」的小事,它们会污染真实数据,而且一旦落库,修复成本是指数级的。
TypeScript 在这里的作用,是把一类错误从「运行时才爆、用户先发现」挪到「编译时就拦、你本地就发现」。
举一个最实在的例子。我们有一个角色阶梯表 ROLE_RANK,用角色名查它的排序值:
ts
const rank = ROLE_RANK[role]; // 如果是裸 JS,这里拿到的可能是 undefined
if (rank >= ROLE_RANK.admin) { /* ... */ }
在裸 JS 里,rank 是 undefined 时这段比较会静默失败(或在某些上下文里阴差阳错地通过),bug 就这么溜进了生产。而当我们打开 noUncheckedIndexedAccess 这个编译选项后,TS 会明确告诉你:rank 的类型是 number | undefined,你必须处理「查不到」的情况(比如补一个 ?? 0)。编译器逼你把这个分支写出来,而不是靠运气。
这就是后端用 TS 的核心价值:类型不是炫技,是给「共享进程里的共享数据」加一道护栏。
二、目录约定:七个目录各管一摊
工程初始化之后,第一件要紧事是定目录。我见过太多项目,所有 .ts 文件平铺在一个 src 里,三个月后谁也分不清哪是路由、哪是业务逻辑、哪是纯类型。我们的 Node 后端,根目录下最终定为七个目录(早期曾有一个 lib/ 放领域工具,后来在结构调优时拆进了 services/ 和 shared/,现已彻底移除):
src/
├── config/ 配置读取(环境变量 → 统一 Env 对象)
├── db/ 数据库连接、迁移与 Drizzle schema
├── middleware/ 中间件(认证、CORS、校验、全局错误捕获)
├── routes/ HTTP 层:解析参数、调 service、包响应信封
├── services/ 业务层:承载领域规则 + 数据库操作
├── shared/ 基础设施(jwt、密码哈希、存储适配、分页、响应信封、错误与错误码)
└── types/ 纯类型定义(不依赖任何运行时)
这七个目录的边界,其实对应一个很重要的认知:后端分层不是「目录装饰」,是「职责边界」。
routes/要薄。它只做三件事:把请求参数解析出来、调用对应的service、把结果包进统一响应信封返回。它不应该直接写 SQL,也不应该塞满if/else业务判断。services/是领域逻辑的归宿。文章的增删改、评论的审核流转、阅读量的去重计数......这些「业务规则」写在这里,而不是散落在路由里。shared/放基础设施:JWT 怎么签、密码怎么哈希、文件怎么存、分页怎么算。这些东西跟具体业务无关,谁都能用。types/只放类型,不写任何运行时代码,是真正的「纯类型」。
这里我先不展开分层为什么这么切(那是 {{LINK:M1-03}} 的主舞台),只想让你记住一件事:目录约定是给未来的自己看的契约。 当你三个月后回来加一个新接口,你该把代码放哪、该去哪找现成的工具,目录结构会替你回答。

真实踩坑(P-05):这套目录能扛住后来的大重构,靠的是一条纪律------全仓只用
@/别名(如@/services/article)和./同目录相对引用,绝对禁止../../跨目录相对引用 。原因很现实:当我们要把lib/拆进services/和shared/时(这件事后面会专门讲),如果文件里到处是../../,移动一个文件就会牵一发动全身,而且 tsc 不会立刻报错,要等到具体引用处才爆。别名 + 同目录相对引用,相当于给「大规模重命名」装了一根保险丝。我们迁移前甚至养成了一个习惯:先grep '\.\./\.\./'确认全仓零命中,再动手。
三、热更新与本地调试
工程能跑起来,比什么都重要。本地开发时,我们希望改一行代码、保存,服务自动重启、立刻看到效果------这就是「热更新」。
我们的 package.json 里,开发脚本长这样:
json
{
"scripts": {
"dev": "tsx watch src/index.ts",
"start": "tsx src/index.ts",
"build": "tsc -p tsconfig.json"
}
}
注意我用的是 tsx watch,不是 ts-node、也不是 nodemon + ts-node。tsx 是基于 esbuild 的 TS 运行器,它直接吃 .ts 文件、不需要先编译成 .js 再跑,启动极快;watch 模式监听文件变化自动重启进程。
为什么不用 nodemon?nodemon 本身只负责「文件变了就重启命令」,它不认识 TS,得配合 ts-node 才能跑 TS,链条长、冷启动慢、配置碎。而 tsx 一个工具把「识别 TS + 热重启」都包了,开发体验清爽很多。
调试这边,因为 tsx 底层走 esbuild 转译、保留了 source map,你在 VS Code 里打断点,命中的就是你的 .ts 源码行,而不是转译后的乱码。这对定位问题至关重要------你调试的是你写的代码,不是编译器吐出来的东西。
还有一个小但关键的点:build 这一步用的是 tsc 而不是 tsx。tsc 负责「类型检查 + 产出可部署的 JS」,它开着 noEmit(只检查不落盘也行,看配置),是上线前的质量闸门之一。开发用 tsx 图快,上线用 tsc 图稳,两者分工明确。
四、门禁雏形:tsc、biome、vitest 三件套
讲完初始化,我想给你种下一个贯穿整套后端工程的意识:门禁(gate)。
所谓门禁,就是「代码能不能算『过关』」的自动化检查。我们本地的工程,从第一天起就有三道:
tsc --noEmit(类型检查) :编译期拦错,前面说的undefined问题就是它抓的。biome check(代码规范 + 静态检查) :统一格式、禁any、查未使用变量等。vitest run(测试):跑单元测试和接口测试,确保行为没被破坏。
这三件套不是我拍脑袋定的,而是行业里反复验证过的「最低防线」。真正有意思的是后面两件事,它们会单独成篇深入讲:
- 测试策略本身是一套学问({{LINK:M1-21}} 会展开:测试金字塔、用
app.request发真实请求、测试库怎么隔离)。 - 除了这三道本地门禁,我们还有一道更特殊的------契约门禁:一份 OpenAPI 文档是「前后端唯一的接口真相源」,任何代码改动都不能偏离它({{LINK:M1-20}} 会讲为什么文档会脱节、又怎么用双门校验守住)。
真实踩坑(P-01):
@/这个别名,在 TypeScript 7 里差点让我们翻车。baseUrl这个配置项在 TS 7 被彻底移除了,如果你还像老教程那样写baseUrl + paths,编译器直接甩你一个TS5102错误,项目起不来。正确做法是删掉baseUrl,只留paths: { "@/*": ["./src/*"] }------paths会相对 tsconfig 自身解析,不需要baseUrl当锚。测试侧(vitest)也要对齐,用resolve.alias['@']指到./src。这事儿看着小,但凡是照着旧博客配 TS 的,十个有八个会卡在这一行。
真实踩坑(P-02):类型纪律不是开个strict: true就完事。我们额外开了两个「硬核」选项。verbatimModuleSyntax: true强制你用import type导入类型------它逼着你在「值」和「类型」之间划清界限,依赖关系更干净。noUncheckedIndexedAccess: true让上面那个ROLE_RANK[role]的类型变成number | undefined,逼你处理查不到的情况。配合 biome 的noExplicitAny(禁any)和「全量避免 any」,这套组合拳下来,很多「运行时才暴露的类型错」在保存的那一刻就被编译器摁住了。
小结
这一篇我们搭起了 Node 后端的工程骨架:
- 用 TypeScript 而不是裸 JS------后端是共享进程、共享数据,类型护栏的失败成本远低于前端。
- 七个目录各管一摊 ------
routes薄、services装领域、shared放基础设施、types纯类型,目录是给未来的自己看的契约。 tsx watch做热更新、tsc做上线前的类型闸门------开发图快、上线图稳。- tsc / biome / vitest 三道门禁------从第一天就给代码质量兜底,后面会逐步加深(测试策略、契约门禁)。
三个真实踩坑你先记个印象:TS 7 删了 baseUrl(P-01)、类型纪律要开硬核选项(P-02)、@/ 别名是重构保险丝(P-05)。它们都不是教科书上的标准答案,是我们在这个工程里真踩过、真改过的。
下一篇({{LINK:M1-02}})我们聊框架选型:Express、Koa、Fastify、NestJS 差在哪,以及为什么这个工程最终选了 Hono。
订阅这个专栏
如果你也想跟着一个真实的多端文章系统,从「前端工程师」走到「能设计全栈系统的人」,欢迎订阅我的《成为全栈开发工程师》专栏。整个 Node 后端篇会用三十一篇,带你从工程初始化一路走到部署上线,欢迎在评论区讨论、指正。
