用 Bun 写一个 RESTful TodoList,顺便把面向接口编程整明白

开场白

前端写久了,有时候会被框架惯坏------Vue 一把梭,React 一把梭,反正尤大和脸书都给你封装好了,干就完了。但写后端的时候,有些概念就绕不过去了,比如接口(Interface) ,比如 RESTful ,比如面向对象编程到底面向了个啥。

这篇文章是我在学 Bun 的时候随手写的一个 TodoList 的复盘。功能很简单------就一个增删查(删还没写,别急),但里面串起来的东西不少:TypeScript 的类型约束、Bun 的原生 HTTP 服务、RESTful 的 URL 设计、前端 fetch 的两种写法。一个一个来。


一、Interface------"先定规矩,再写代码"

ts 复制代码
interface Todo {
  id: string;
  title: string;
  completed: boolean;
  createdAt: Date;
}

这是整个项目的第一段代码。可能有人会觉得:不就定义了一个对象长什么样吗,有啥好说的?

但 Interface 其实是 OOP(面向对象编程)里最容易被人忽略的核心概念。OOP 三大件------封装、继承、多态------大家都背得滚瓜烂熟,但接口才是设计模式的地基

你可以这样理解:

  • 面向对象编程是地基------教你用类和对象来组织代码;
  • 面向接口编程是盖楼的图纸------它不关心你用什么砖、什么水泥,只关心"这个楼必须有电梯、消防通道、停车场"。

换句话说,Interface 声明的是约束 ,而不是实现。上面的 Todo 接口就是在说:任何一个 Todo 对象,必须有 id、title、completed、createdAt 这四个属性,少一个都不行,类型错了也不行。

这和抽象类有什么区别?简单粗暴地说:

Interface 抽象类
能不能有实现 ❌ 不能,纯声明 ✅ 可以有方法体
一个类能实现几个 多个 只能继承一个
本质 契约/规范 模板/基类

所以在这个项目里,我们用 Interface 定义 Todo 的形状,TypeScript 编译器帮你在写代码的时候就检查------少个字段、类型写错,根本跑不起来。这就是编译时的安全感


二、RESTful------"一切皆资源"

RESTful 的核心思想其实就一句:一切皆资源

RESTful 不是什么高深的架构,它本质上就是一种定义 URL 的规则

复制代码
资源的名词 + 资源的操作(HTTP 动词)

拿我们这个 TodoList 举例:

操作 HTTP 动词 URL
获取所有任务 GET /todos
获取单个任务 GET /todos/:id
创建任务 POST /todos
更新任务 PUT/PATCH /todos/:id
删除任务 DELETE /todos/:id

注意到了吗?URL 里只有名词 (todos),操作全靠 HTTP 动词来表达。这就是 RESTful 的精髓------URL 代表资源,动词代表操作 ,不搞那些 /getTodoList/createTodo 这种 RPC 风格的鬼东西。

代码里长这样:

ts 复制代码
// 获取所有 todos
if (req.method === 'GET' && url.pathname === '/todos') {
  return Response.json(todos, { headers });
}

// 获取单个 todo
if (req.method === 'GET' && url.pathname.startsWith('/todos/')) {
  const id = url.pathname.split("/")[2];
  const todo = todos.find((todo) => todo.id === id);
  return Response.json(todo);
}

两个分支,用 req.methodurl.pathname 组合判断------这就是最小可行路由。真正的项目你肯定会用路由库,但手写过一次,你才知道那些框架到底帮你做了什么。

至于"路由"这个词,有个很土的比喻------路由器就像一个交警,站在路口根据请求的目的地把流量导向不同的处理函数。挺土的,但确实好懂。


三、Bun.serve------"一个对象起一个服务"

Node.js 起一个 HTTP 服务要写多少行?

js 复制代码
const http = require('http');
const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify({ msg: 'hello' }));
});
server.listen(3000);

Bun 呢:

ts 复制代码
const server = Bun.serve({
  port: 8081,
  async fetch(req) {
    // 在这里处理请求
  }
});

一个对象字面量,一个 fetch 函数,搞定。没有 createServer,没有 listen,没有 writeHead......Bun 把这些都包进了 Bun.serve,你只需要关心端口号和请求处理逻辑。

这里有一个很多人容易踩的坑:fetch 函数里的 req 是请求对象url.pathname 是路径,url.searchParams 是查询参数。拿 URL 来说:

bash 复制代码
https://baidu.com:8080/pathname?page=1&size=10
  • protocol: https
  • hostname: baidu.com
  • port: 8080
  • pathname: /pathname
  • search: ?page=1&size=10

这些在 new URL(req.url) 之后全能拿到。别自己用正则去拆,太费劲了。

另外,服务端返回 JSON 的时候,记得带上 CORS 头:

ts 复制代码
const headers = {
  'Access-Control-Allow-Origin': '*'
};

不然后端跑起来了,浏览器一个跨域报错糊你脸上,排查半天才发现是忘了加 header。这种亏我吃过。


四、前端------从 Promise 链到 async/await

前端部分很简单,就是一个 HTML 文件里嵌了一段 JS。但这段代码展示了两种写法,刚好能看出进化过程:

写法一:Promise 链

js 复制代码
fetch("http://127.0.0.1:8081/todos")
  .then(res => res.json())
  .then(data => {
    todos.innerHTML = data.map(todo => `<li>${todo.title}</li>`).join('');
  });

写法二:async/await

js 复制代码
async function main() {
  const res = await fetch("http://127.0.0.1:8081/todos");
  const data = await res.json();
  todos.innerHTML = data.map(todo => `<li>${todo.title}</li>`).join('');
}
main();

两种写法做的事情一模一样,但可读性天差地别。Promise 链写多了嵌套,就是经典的"回调地狱"青春版;async/await 把异步代码写成了同步的样子,逻辑流一目了然。

我个人习惯:超过两个 .then() 就直接换 async/await,别折磨未来的自己。


五、这个项目还缺什么?

老实说,这个 TodoList 目前只实现了 R(Read),CRUD 里 C、U、D 都没写。如果你准备把它补全,可以考虑:

  1. POST /todos ------ 从 req.body 里解析 JSON,往数组里 push,返回 201;
  2. PUT /todos/:id ------ 找到对应的 todo,更新 completed 状态(勾选/取消);
  3. DELETE /todos/:id ------ splice 掉指定 id 的任务;
  4. 数据持久化 ------ 目前数据在内存里,服务重启就没了。可以用 Bun 的 SQLite 支持或者直接写 JSON 文件;
  5. 前端交互 ------ 加个输入框和按钮,别只会展示了。

最后几句话

回过头看,这个不到 100 行的小项目串起来的东西不少:

  • TypeScript 的 Interface 教会你"先定契约,再写逻辑";
  • RESTful 教会你"用 URL 描述资源,用 HTTP 动词描述操作";
  • Bun 让你知道起一个后端服务可以有多简单;
  • async/await 把你从 Promise 链里解救出来。

小项目有小项目的价值------它不会让你迷失在目录结构和中间件里,能让你专注地把几个核心概念吃透。收工。

相关推荐
英勇无比的消炎药1 小时前
别再盲目混用AI组件库和传统组件库差距原来这么大
前端·vue.js
ViavaCos1 小时前
AI 帮我写代码,我帮 AI 踩坑:Vue 大数据表格优化全记录
前端·性能优化
lichenyang4531 小时前
聊天消息的「状态」该怎么存?从一堆 boolean 到一个状态机
前端
gz-郭小敏2 小时前
优化横向滚动展示大量数据的时候数据晃动问题
前端·javascript·html·css3
ClouGence2 小时前
自动化测试 CueCast 新版本发布:录制更稳、回放更准、排障更清晰
前端·程序员·测试
骑士雄师2 小时前
19.3 langgraph的工作节点和路由函数
java·前端·数据库
小小小小宇2 小时前
TypeScript类型体操
前端
喜欢踢足球的老罗2 小时前
一张跨域图的“四次换乘“:blob URL 与 Chrome 扩展架构里的工程艺术
前端·chrome·架构
程序员黑豆2 小时前
AI全栈开发 - Java:基本数据类型 vs 引用数据类型的内存存储
java·前端·ai编程