Bun + TypeScript:AI 时代的后端开发入门
写在前面:这次我第一次接触到了后端开发的全貌。从一个比 Node 更快的运行时 Bun 开始,到用 TypeScript 给 JavaScript 加上类型铠甲,再到亲手搭起一个 HTTP 服务器,最后用 axios 向大模型发起请求------这条链路跑通之后,我突然意识到,所谓的"全栈"并不是遥不可及的术语,而是一套可以亲手触摸的工具链。
为什么 AI Agent 都在用 TypeScript?
开局先问了大家一个问题:为什么现在 AI Agent 的开发,几乎清一色都在用 TypeScript?
首先让我们来初步了解一下 TypeScript :
TypeScript 来自微软,是 JavaScript 的超集,它的核心作用就是添加了类型约束。
JavaScript 是弱类型语言,这个特性很多时候会产生问题。比如浏览器里一个 input 输入框,我们以为用户输入的是数字,但实际上拿到的是字符串。再比如 + 这个运算符,它既可以是加法,也可以是字符串拼接------如果两个操作数类型不一样,JavaScript 不会报错,而是默默地帮你做类型转换,结果可能完全出乎你的意料。
更麻烦的是,这些错误在编译阶段不会报错。它们会藏在系统里,直到某个特定场景才突然炸开。对于个人小项目来说这可能无伤大雅,但对于企业级开发,或者对于一个需要稳定运行的 AI Agent 来说,这就是一颗定时炸弹。
TypeScript 的解决方案很直接:静态类型编译。它会在编译阶段就把 TS 文件转成 JS 文件,同时检查类型错误和代码错误。换句话说,它让你在写代码的时候就能发现问题,而不是等到运行时才被用户投诉。
TS 已经非常强大,已经成为 AI Agent 的标配,甚至 Claude Code 泄露的源代码里也有它的身影。我们应该意识到,学 TS 不只是一个"更好的 JS"那么简单,而是在跟上一套正在形成的行业标准。
Bun:Node 的升级版,还是替代品?
理解了 TypeScript 的必要性之后,接下来引入这节课的核心工具:Bun。
大家肯定对 Node.js 并不陌生。不过 Bun 的定位肯定能让大家耳目一新
Bun 是比 Node 更快、开箱即用、0 配置的 JS/TS 运行时 + 包管理器。
三个关键词:更快 、开箱即用 、0 配置。对于一个经常被环境配置折磨的人来说,"0 配置"这三个字本身就足够有吸引力了。
更让我意外的是,在查询资料的时候发现 Bun 的母公司被 Anthropic 收购了,而收购的目的就是用于对 Claude Code 的底层优化。这让我隐隐有点感觉,为什么 Claude Code 现在是被程序员们公认的最强大的编程 Agent ------原来它背后可能就有 Bun 的影子。这种"工具链之间的关联",是我之前没有想过的。
安装 Bun 的命令也很简单,在 Windows PowerShell 里执行:
powershell
powershell -c "irm bun.sh/install/windows | iex"
安装完成后,bun 命令就可以直接用了,不需要像 Node 那样还要手动配环境变量或者处理各种版本冲突。
TypeScript 实战:从变量类型到函数约束
Bun 装好了,接下来就要写代码了。强调了一个点:Bun 原生支持 TypeScript ,不需要像 Node 那样还要配 ts-node 或者先编译再运行。这对我这种怕麻烦的人来说,又是一个加分项。
我们先从最基础的变量类型开始。这是我笔记里的第一段代码:
typescript
// TypeScript -- AI Agent 使用的语言,是 JavaScript 的超集
// JS 是弱类型语言,es6+ 之后 JS 意在参与企业级项目开发,但是弱类型语言还是容易出现问题
// 微软推出了 Node.js,但是 Node.js 的社区发展一般,目前的使用不是非常顺畅
// 之后 Bun 公司横空出世,Bun 高性能
// ts 可以让我们像写 js 一样,但是 ts 添加了类型约束
const nickname: string = '9527';
const age: number = 95;// 会检查我们的类型错误
console.log(`我是${nickname}, 我今年${age}岁`);
这里 :string 和 :number 就是类型注解。如果我后面不小心写了 age = '95',TypeScript 在编译时就会直接报错,而不是像 JavaScript 那样默默接受。
理解了变量类型后,让我们进入了函数的类型约束。这是笔记里的第二段代码:
typescript
function add(a: number, b: number): number {
return a + b;// + 除了是加法,还可以是字符串拼接
}
// JS 足够简单
// 大型项目中,类型错误可能会出现在任何地方
let a = 1;
let b = '2';
// console.log(add(a,parseInt(b))); // API
// console.log(add(a,Number(b))); // Number(b) 会将字符串转换为数字 强制类型转换
console.log(add(a,+b)); // 隐式类型转换
// 返回值类型约束
这段代码里藏着好几个知识点。
首先是函数参数的类型约束:a: number 和 b: number 确保了这个函数只能接受数字。返回值类型 : number 则确保了函数的输出也一定是数字。
然后是类型转换的三种方式:
parseInt(b):显式转换,把字符串转成整数Number(b):显式转换,把字符串转成数字+b:隐式类型转换,用一元加号运算符把字符串转成数字
特别强调一下 +b 这种隐式转换。它的好处是写法简洁,但坏处是可读性变差------如果团队里有人不熟悉这个技巧,看到 +b 可能会愣一下。我建议大家把三种方式都记下来,因为大型项目中类型错误可能会出现在任何地方,多一种转换方式就多一种处理手段。
Promise 与 async/await:把异步变成同步的样子
类型约束讲完之后,让我们插入一个关于 JS 的知识点,来聊聊 JS 的执行机制:异步编程。
这是 JavaScript 里躲不开的一个坎。让我们封装一个 sleep 函数,让程序暂停指定的时间:
javascript
// 如何封装一个 sleep 函数? 200ms?2000ms?
async function main() {
console.log('start');
// await 后面 接受一个 Promise 对象
await sleep(2000);// 异步任务同步化
console.log('end');
}
function sleep(t) {
// es6 中提供的,解决异步问题的 api ,许下诺言
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, t);
});
}
main();
这段代码相信让大家对 Promise 有了更直观的理解。
Promise 是 ES6 引入的,用来解决异步问题。大家可以相信成"许下诺言"------你发起一个异步操作,然后"许诺"在操作完成时告诉你结果。resolve 表示诺言兑现,reject 表示诺言破裂。
async/await 的妙处在于异步任务同步化 。如果没有 await,sleep(2000) 后面的 console.log('end') 会立刻执行,不会等 2 秒。但加了 await 之后,代码的执行顺序看起来就像同步代码一样:start → 等 2 秒 → end。
所以异步编程的核心不是让代码变复杂,而是找到一种让逻辑更清晰表达方式。
Interface:面向接口编程,不只是口号
接下来我们进入了一个更"工程化"的话题:Interface(接口)。
接口的定义很清晰:
接口用于声明一个对象的约束,一个对象应该拥有哪些属性或方法。
这不是一个抽象的概念,而是面向对象编程(OOP)的核心。笔记里列出了 OOP 的三大特性:
- 封装
- 继承
- 多态
注意:面向对象编程是基础,面向接口编程是设计模式的基础。我希望大家意识到,接口不只是 TypeScript 的一个语法糖,而是一种设计思想------先定义规范,再实现规范。
除了接口,还有另一个重要概念:RESTful API。
定义是:
定义 url 的规则,资源的名词 + 资源的操作(http 动词)有一定的语义规则,也就是 restful 规则。
核心思想是一切皆资源 。不同的资源放在不同的路径上,用 HTTP 方法(GET、POST、PUT、DELETE)来表示对资源的操作。这种设计让 URL 本身就有了语义,不需要靠文档去解释 /getUserInfo 和 /deleteUser 有什么区别。
理解了接口和 RESTful 之后,接下来,我们用 Bun 搭一个真正的 HTTP 服务器。
Bun.serve:用几行代码搭起后端服务
看看我写的后端服务:
typescript
// 任务资源
// ts = js + 强类型
// 如何约束 todos 的类型 => 为什么要约束 todos 的类型? => 为了任务的准确性
// 自定义类型对象 通过接口约束
// 面向对象的核心概念
interface Todo {
// 接口定义区,对象字面量采用, 分隔
// 接口定义区,属性采用: 分隔
id: string;
title: string;
completed: boolean;
createdAt: Date;// 任务创建时间
}
// 资源
const todos: Todo[]= [
{
id: '1',
title: '学习ts',
completed: false,
createdAt: new Date(),
},
{
id: '2',
title: '睡觉',
completed: false,
createdAt: new Date(),
},
{
id: '3',
title: '吃饭',
completed: false,
createdAt: new Date(),
}
];
// 内置了 一个高性能的服务器
const server = Bun.serve({
port: 8080, // 127.0.0.1:8080 IP 地址对应服务器,端口对应具体服务进程
// IP 对应一个服务器,不同的端口提供不同的服务
// 例如 http 服务,mail 服务,音乐 服务等
// http 服务器 处于 伺服 状态,http 是一个基于请求(request)响应(response)的协议
// 只能单方面的接受用户请求
// webstock 协议,通信协议,双方都可以发送请求和响应
// 用户通过在浏览器中输入 url 带上端口号,去发送请求(req对象,可以有多个)
// server 通过 fetch 函数(是bun.serve 的内置的一个方法,所有的请求都会在这里处理)
// 后端的本质是 http 协议 和 web 服务
async fetch(req) {
const headers = {
// * 通配符,允许所有域名访问
'Access-Control-Allow-Origin': '*'
}
// 异步任务,控制流程 await
console.log(req);
// https:// 协议版本 + 域名 + 端口号 + 路径 + 查询参数 + 查询字符串(query string)
// https:// 127.0.0.1:8080/todos
// https:// 127.0.0.1:8080/todos?id=1
const url = new URL(req.url); // 拿到用户访问的 url 的内置的资源(端口号等...)
if(url.pathname === '/todos'){
return Response.json({msg:'获取todos列表',todos}, {headers});
}
// url.pathname 是用户访问的路径 string ,startsWith('/todos/') 是判断是否以 /todos/ 开头
// todos/:id 详情
if(req.method === 'GET' && url.pathname.startsWith('/todos/')){
// 从 url 中获取 id
const id = url.pathname.split('/')[2];
// 从 todos 中根据 id 查找对应的 todo
const todo = todos.find((todo) => todo.id === id);
return Response.json({msg:'获取todo详情',todo}, {headers});
}
return Response.json({msg:'hello world'}, {headers});
}
});
首先是 interface Todo,它定义了一个任务应该有哪些字段:id、title、completed、createdAt。然后用 const todos: Todo[] 声明了一个任务数组,这里的 : Todo[] 就是 TypeScript 的类型约束------确保数组里的每一个对象都符合 Todo 接口的规范。
然后是 Bun.serve,这是 Bun 内置的高性能服务器。它只需要一个配置对象,最核心的就是 fetch 函数------所有进入服务器的 HTTP 请求都会在这里处理。
接下来对几个概念做下解释:
port: 8080:IP 地址对应服务器,端口号对应具体的服务进程。一台服务器可以跑很多服务,HTTP 服务、邮件服务、音乐服务,它们靠端口号来区分。- HTTP 是基于请求-响应的协议,服务器只能被动接受请求。如果是 WebSocket,双方就都可以主动发消息了。
- URL 的结构:
协议版本 + 域名 + 端口号 + 路径 + 查询参数 + 查询字符串
在 fetch 函数里,我用 new URL(req.url) 解析了用户请求的 URL,然后根据 url.pathname 做路由判断:
/todos→ 返回所有任务列表/todos/:id→ 返回指定 ID 的任务详情- 其他路径 → 返回
hello world
Response.json() 是 Bun 提供的便捷方法,直接把对象转成 JSON 格式的 HTTP 响应。headers 里的 'Access-Control-Allow-Origin': '*' 是为了解决跨域问题,允许任何域名访问这个接口。
后端开发并没有想象中那么神秘------它本质上就是解析请求、处理逻辑、返回响应。
axios:向大模型发起第一次 HTTP 请求
搭好了服务器之后,我们就进入了这节课的最后一个环节:用 axios 调用大模型 API。
笔记里的代码是这样的:
typescript
console.log("Hello via Bun!");
// axios 是一个基于 Promise 的 HTTP 客户端
// http 请求 LLM 接口
// bun 代替 npm 做包管理器
import axios from 'axios';
import dotenv from 'dotenv';
dotenv.config();
async function chat(){
// llm 可能会出错,异常
// 1.请求超时 timeout network ,2.llm 忙,3.api key 无效
try {
// GET 请求 文本内容有上限
// apikey GET 不安全 明文传输
// 图片 上传 post 的请求体可以传输
// Http 请求分为三部分 但是 GET 请求没有请求体
// 1.请求行 包含 url ,method,http version
// 2.请求头 Authorization 包含 api key
// 3.请求体 body
// axios 是一个请求的框架,封账了 fetch , 企业级别的
// fetch http 请求 api
const res = await axios.post(`${process.env.OPENAI_API_BASE_URL}`,{
model: process.env.OPENAI_MODEL,
messages: [
{
role: 'user',
content: '你好,介绍一下 Bun'
}
]
},
{
headers: {
// 传给 http 的文本类型
'Content-Type': 'application/json',
// 验证 api key 是否正确
'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`
}
}
);
// axios 默认会在响应前面带上 data 字段
// 我们需要从 data 字段中提取出实际的响应内容
console.log(res.data.choices[0].message.content);
} catch (err: any) {
console.log(err.message);
}
}
chat();
axios 是一个基于 Promise 的 HTTP 客户端,它"封装了 fetch,企业级别的"。相比浏览器原生的 fetch,axios 的 API 更友好,而且自动处理了一些底层细节。
HTTP 请求的三部分结构:
- 请求行:包含 URL、HTTP 方法、HTTP 版本
- 请求头 :包含各种元数据,比如
Authorization里放 API Key - 请求体(body):POST 请求可以带请求体,GET 请求没有
这就可以理解为什么调用 LLM 接口要用 POST:GET 请求没有请求体,文本内容有上限,而且 API Key 如果放在 URL 里会明文传输,不安全。POST 请求可以把参数放在 body 里,把 API Key 放在 header 里,这才是正确的做法。
代码里用了 dotenv 来管理环境变量,process.env.OPENAI_API_KEY 从 .env 文件里读取配置。这是企业级项目的标配,避免把敏感信息硬编码在代码里。
请求体遵循了 OpenAI 的 API 规范:
model:指定使用的模型messages:对话历史,每条消息有role和content
响应的处理也很有意思:axios 默认会在响应外面包一层 data 字段,所以真正的响应内容是 res.data.choices[0].message.content。这个结构是 OpenAI API 的返回格式,choices 是一个数组,因为某些接口可能返回多个候选答案。
错误处理用了 try/catch,笔记里列出了三种可能的异常:
- 请求超时(网络问题)
- LLM 服务忙
- API Key 无效
我第一次跑这段代码的时候,遇到了 API Key 配置错误的问题。改好 .env 文件后,终端里真的输出了大模型对 Bun 的介绍。那种"自己的代码在跟 AI 对话"的感觉,和直接用 ChatGPT 网页版完全不同。
从类型到服务:现在怎么理解这节课
这节课的内容很多,从 TypeScript 的类型约束,到 Bun 的运行时,再到 HTTP 服务器和 axios 请求。但把它们串起来看,其实是一条很清晰的学习路径:
类型 → 异步 → 接口 → 服务器 → API 调用
TypeScript 解决了 JavaScript 的"不靠谱"问题,让我们在写代码的时候就能抓住错误。Bun 解决了环境配置的繁琐问题,让我们可以零配置地运行 TS。Promise 和 async/await 解决了异步代码的"回调地狱"问题。Interface 和 RESTful 解决了代码组织和 API 设计的问题。最后,axios 和 Bun.serve 让我们可以把这些知识变成真正可用的服务。
在 AI 时代,程序员当然还要懂代码,但更重要的能力可能是把散落的工具和技术串成一条完整链路的能力。这节课介绍了跑通"写类型约束 → 搭服务器 → 调 AI 接口"的完整流程,这种成就感远比单独学会某个语法点更强烈。
有句话我印象很深:"后端的本质是 http 协议 和 web 服务"。以前我觉得后端是黑盒,是高不可攀的技术壁垒。但现在我明白了,它的本质并不复杂------只是需要理解的细节很多。而 Bun 和 TypeScript 做的,正是把这些细节尽可能地封装起来,让开发者可以专注于业务逻辑本身。
这节课没有复杂的算法,没有晦涩的原理,但我希望能让你觉得,全栈开发离我们并没有那么远。