news 2026/6/12 10:30:55

从零搭建一个 RESTful Todo 服务 —— Bun + TypeScript 全栈最小闭环

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从零搭建一个 RESTful Todo 服务 —— Bun + TypeScript 全栈最小闭环

本文通过一个极简的**任务清单(Todos)**项目,一步步理解如何用BunTypeScript搭建一个 RESTful 风格的后端服务,并配合前端页面完成数据展示。文章按"建模 → 存储 → 服务 → 路由 → 消费"的逻辑线展开。


目录

  1. 项目概览
  2. 数据建模:用 interface 定义资源
  3. 数据存储:内存中的"数据库"
  4. HTTP 服务:Bun.serve 启动服务器
  5. RESTful 路由:一切皆资源
    • 5.1 获取全部任务GET /todos
    • 5.2 获取单个任务GET /todos/:id
  6. CORS 跨域:让前端能访问后端
  7. 前端消费 API
    • 7.1 Promise + then 链式调用
    • 7.2 async/await 异步语法
  8. 运行项目
  9. 总结

1. 项目概览

整个项目只有3 个文件,结构极其精简:

todos/ ├── server.ts # 后端服务(核心) ├── index.html # 前端页面 └── readme.md # 概念笔记

数据流非常简单:

浏览器 (index.html) ──GET /todos──▶ Bun 服务器 (server.ts) ──查找──▶ todos 数组 ◀──JSON 数据──

2. 数据建模:用 interface 定义资源

在面向对象编程(OOP)中,接口(interface)用于声明一个对象"长什么样"——它必须具备哪些属性和方法。就像给数据签了一份合同,确保后续所有操作都遵循同一套结构。

interfaceTodo{id:string;title:string;completed:boolean;createdAt:Date;}

逐字段解释:

字段类型含义
idstring任务的唯一标识
titlestring任务名称,如"吃饭"
completedboolean是否完成
createdAtDate创建时间

interface只在编译时做类型检查,编译后不产生任何 JS 代码——零运行时开销。你获得了类型安全,却不损失性能。


3. 数据存储:内存中的"数据库"

为了保持示例简单,我们用一个数组代替数据库:

consttodos:Todo[]=[{id:"1",title:"吃饭",completed:false,createdAt:newDate()},{id:"2",title:"睡觉",completed:false,createdAt:newDate()},{id:"3",title:"打豆豆",completed:false,createdAt:newDate()},];

声明: Todo[]意味着这个数组只能存放符合Todo接口的对象。如果你不小心写了{ id: 1, title: "x" }(缺少completedcreatedAt),TypeScript 会在编码阶段就报错,而不是等到运行时才发现问题——这就是类型系统的价值。


4. HTTP 服务:Bun.serve 启动服务器

下面这行代码是整个后端的心脏:

constserver=Bun.serve({port:8080,asyncfetch(req){// 所有 HTTP 请求都会进入这个函数}})

解释几个关键点:

  • port: 8080:服务器监听127.0.0.1:8080。IP 地址对应一台机器,端口号区分同一台机器上的不同服务(HTTP、邮件、音乐服务等)。
  • fetch(req):这是 Bun.serve 的内置方法,每一个到达服务器的 HTTP 请求都会被传入这个函数。req对象包含了请求的所有信息(方法、路径、头信息等)。
  • HTTP 协议的本质:请求(Request)→ 响应(Response)。浏览器发送一个 Request,服务器处理后返回一个 Response。

Bun 内置了 TypeScript 支持和 HTTP 服务,不需要安装任何第三方依赖。


5. RESTful 路由:一切皆资源

RESTful的核心理念是"一切皆资源"。URL 路径对应资源名词,HTTP 方法对应操作动词

HTTP 方法含义示例
GET读取资源GET /todos获取全部任务
POST创建资源POST /todos新建任务
PUT更新资源PUT /todos/1修改任务 1
DELETE删除资源DELETE /todos/1删除任务 1

本项目目前实现了两个GET路由。

5.1 获取全部任务GET /todos

consturl=newURL(req.url);// 解析用户访问的 URLif(req.method==='GET'&&url.pathname==="/todos"){returnResponse.json(todos,{headers});}

逻辑拆解:

  1. new URL(req.url):将浏览器的请求地址(如http://127.0.0.1:8080/todos)解析为一个 URL 对象,方便提取pathname
  2. 条件判断:同时检查请求方法(GET)和路径(/todos),精确匹配。
  3. Response.json(todos):将 TypeScript 数组自动序列化为 JSON 格式返回。

5.2 获取单个任务GET /todos/:id

if(req.method==='GET'&&url.pathname.startsWith("/todos/")){constid=url.pathname.split("/")[2];// "/todos/3" → split("/") → ["", "todos", "3"] → 取 [2] 得 "3"consttodo=todos.find((t)=>t.id===id);returnResponse.json(todo);}

逻辑拆解:

  1. startsWith("/todos/"):用前缀匹配,因为后面跟着动态的id
  2. split("/")[2]:从路径中提取id。例如/todos/2分割后得到["", "todos", "2"],取下标[2]
  3. find():在数组中查找匹配项。如果找不到,返回undefined

6. CORS 跨域:让前端能访问后端

浏览器的同源策略默认禁止不同域名/端口之间的请求。前端页面通常通过file://协议打开,与http://127.0.0.1:8080属于不同源,因此需要服务端放行:

constheaders={'Access-Control-Allow-Origin':"*"}
  • Access-Control-Allow-Origin: *:允许任何来源的请求访问该接口。
  • 这个 headers 对象被注入到每一个Response.json()的返回中。

⚠️ 星号*仅适用于开发环境。生产环境中应指定具体的域名。


7. 前端消费 API

有了后端服务,前端页面通过fetchAPI获取数据。项目中展示了两种写法:

7.1 Promise + then 链式调用

fetch("http://127.0.0.1:8080/todos").then(res=>res.json())// 将 Response 转换为 JSON.then(data=>{// 拿到真正的数据todos.innerHTML=data.map(todo=>`<li>${todo.title}</li>`).join('');});

执行流程:

fetch() → 发送 HTTP 请求 .then(res => res.json()) → 等待响应,将 body 解析为 JSON .then(data => ...) → 拿到解析后的 JS 对象

链中的每一步都在等上一步完成后才执行——这就是 Promise 的异步模型。

7.2 async/await 异步语法

asyncfunctionmain(){constres=awaitfetch("http://127.0.0.1:8080/todos");constdata=awaitres.json();todos.innerHTML=data.map(todo=>`<li>${todo.title}</li>`).join('');}main();

对比两种写法:

维度.then()async/await
可读性嵌套较多时易混乱像同步代码,直观
错误处理.catch()try/catch
本质Promise 的原生方法Promise 的语法糖

await后面的表达式必须是一个Promisefetch()res.json()都返回 Promise,所以都可以await


8. 运行项目

确保已安装 Bun,然后执行:

# 启动后端服务bun run server.ts# 服务运行在 http://127.0.0.1:8080

然后用浏览器打开index.html,或者直接访问:

  • http://127.0.0.1:8080/todos— 获取全部任务(JSON)
  • http://127.0.0.1:8080/todos/1— 获取 id 为 1 的任务(JSON)

9. 总结

这个不到 70 行的项目,完整串联了以下知识体系:

┌─────────────────────────────────────────────────┐ │ TypeScript │ │ interface → 类型约束 → 编译时检查 │ ├─────────────────────────────────────────────────┤ │ OOP 思想 │ │ "面向接口编程" → 上层不依赖底层实现 │ ├─────────────────────────────────────────────────┤ │ Bun 运行时 │ │ Bun.serve → 内置 HTTP 服务 → 零依赖 │ ├─────────────────────────────────────────────────┤ │ RESTful 设计 │ │ 资源 URL + HTTP 动词 → 语义化的 API │ ├─────────────────────────────────────────────────┤ │ 前端消费 │ │ fetch + Promise → async/await 演进 │ └─────────────────────────────────────────────────┘

这条链路从数据建模开始,到服务暴露,再到前端消费,构成了一个完整的"全栈最小闭环"。理解了这个例子,你就掌握了现代 Web 开发的骨架。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/12 10:30:54

STM32H743项目实战:避开总线架构‘坑’,优化DMA与LTDC访问AXI SRAM的性能

STM32H743实战&#xff1a;破解AXI总线瓶颈的五大黄金法则当你在深夜调试STM32H743的LTDC界面时&#xff0c;突然发现屏幕刷新率卡在30fps上不去——这可能是AXI总线在对你发出警告。作为经历过三次产品召回的老工程师&#xff0c;我想分享几个用血泪换来的实战经验。1. 总线架…

作者头像 李华
网站建设 2026/6/12 10:28:57

终极解决方案:如何在Blender中完美导入和编辑MMD模型与动画

终极解决方案&#xff1a;如何在Blender中完美导入和编辑MMD模型与动画 【免费下载链接】blender_mmd_tools MMD Tools is a blender addon for importing/exporting Models and Motions of MikuMikuDance. 项目地址: https://gitcode.com/gh_mirrors/bl/blender_mmd_tools …

作者头像 李华
网站建设 2026/6/12 10:27:44

如果你的 git stash list 中存在多个记录 怎么搞?

如果你的 git stash list 中存在多个记录(例如 stash@{0}, stash@{1} 等),处理逻辑是一样的,但你需要明确你要恢复哪一个。 git stash 的编号是先进先出(LIFO)的,stash@{0} 永远是最新一次存入的内容。如果你有多个 stash,请遵循以下流程: 1. 确认每个 Stash 里存了…

作者头像 李华
网站建设 2026/6/12 10:27:15

JetBrains IDE试用期重置终极指南:如何轻松获得无限试用时间

JetBrains IDE试用期重置终极指南&#xff1a;如何轻松获得无限试用时间 【免费下载链接】ide-eval-resetter 项目地址: https://gitcode.com/gh_mirrors/id/ide-eval-resetter 你是否曾因为IntelliJ IDEA、PyCharm或WebStorm的30天试用期到期而中断开发工作&#xff1…

作者头像 李华
网站建设 2026/6/12 10:24:03

别再搞混了!CAPL编程中Message和结构体的5个核心区别(附避坑指南)

CAPL编程中Message与结构体的5个核心差异解析在汽车电子测试领域&#xff0c;CAPL&#xff08;CAN Access Programming Language&#xff09;是工程师们不可或缺的工具。许多从C/C转型而来的工程师常常会将Message与结构体混为一谈&#xff0c;这种误解往往导致脚本报错、测试结…

作者头像 李华
网站建设 2026/6/12 10:23:57

PGGAN(ProGAN)的‘平滑过渡’到底多重要?一个参数α如何稳定训练并避免‘棋盘效应’(含代码调试技巧)

PGGAN中α参数的艺术&#xff1a;从数学原理到实战调参的深度解析当你在深夜调试PGGAN模型时&#xff0c;是否曾被突然出现的棋盘状伪影惊醒&#xff1f;那些整齐排列的方格像是对开发者无情的嘲讽。而解决这个问题的钥匙&#xff0c;正藏在那个看似简单的α参数里——它不仅是…

作者头像 李华