Bun 内置服务器、 SQL 与数据库
Bun.serve 写 HTTP 服务,bun:sqlite 与 Bun.SQL 两种数据库路径,内置 Redis 客户端,一个运行时完成全栈。
0. 一句话理解
Bun 把服务器、SQL、Redis 客户端都”内置”了:
Bun.serve起服务、bun:sqlite/Bun.SQL查数据库、redis连缓存,少装一半依赖。
本篇围绕”演出周边商店”写一个可运行的小后端:查商品(SQLite)、记订单(统一 SQL API)、做点击计数(Redis),体验零驱动的全栈开发。
1. Bun.serve:HTTP 服务器与路由表
// server.ts
const server = Bun.serve({
port: 3000,
// routes 路由表(Bun 1.2.3+):字面量路径直接命中,:param 声明动态段
routes: {
"/": () => new Response("你好,Bun!"),
"/api/time": () => Response.json({ time: new Date().toISOString() }),
"/api/goods/:id": (req) => {
const { id } = req.params // 动态段自动解析
return Response.json({ id, name: `周边-${id}` })
}
},
// 未命中路由表时兜底,适合复杂判断或返回 404
async fetch(request) {
return new Response("Not Found", { status: 404 })
}
})
console.log(`服务已启动: http://localhost:${server.port}`)
bun server.ts
# curl http://localhost:3000/api/goods/c001
# {"id":"c001","name":"周边-c001"}
讲解:
Bun.serve({ fetch })使用 Web 标准 Request/Response;routes路由表是 1.2.3 起的推荐写法,字面量匹配性能最好,:param动态段从req.params读取。Response.json(...)是 Web 标准便捷方法,自动设置Content-Type: application/json。- 路由未命中落到
fetch兜底——本篇返回 404,避免未知路径静默返回 200;复杂的”方法 + 路径”判断也写在兜底里。 Bun.serve支持读写一体 Cookie(request.cookies)、WebSocket 处理器与 HTML imports,见WebSocket 与前端开发服务器;生产可按需开启 TLS。
2. bun:sqlite:内置 SQLite
// db.ts
import { Database } from "bun:sqlite"
const db = new Database("app.db")
db.run(`
CREATE TABLE IF NOT EXISTS goods (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
stock INTEGER DEFAULT 0
)
`)
// 插入
const insert = db.query("INSERT INTO goods (title, stock) VALUES (?, ?) RETURNING *")
const created = insert.get("魔法未来应援棒", 42)
// 查询
const all = db.query("SELECT * FROM goods ORDER BY id DESC").all()
console.log("创建:", created)
console.log("全部:", all)
讲解:
bun:sqlite是内置 SQLite 驱动,new Database("app.db")打开(没有则创建)数据库文件;单文件、零服务,适合桌面工具、小型网站与本地缓存层。db.run执行建表等无返回语句;db.query(...)预编译 SQL,?是参数占位符,防止 SQL 注入;高频查询复用编译结果更快。insert.get(...)执行插入并返回第一行(RETURNING *返回新记录),.all()返回所有行。- 参数化查询是铁律:永远不要用字符串拼接拼 SQL。
- 事务(保证”扣库存 + 写订单”两步同生共死)与命名参数的进阶用法见Bun SQLite 与 S3。
3. 组合:带数据库的 API
// api.ts
import { Database } from "bun:sqlite"
const db = new Database("app.db")
db.run(`CREATE TABLE IF NOT EXISTS goods (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
stock INTEGER DEFAULT 0
)`)
Bun.serve({
port: 3000,
async fetch(request) {
const url = new URL(request.url)
if (request.method === "POST" && url.pathname === "/goods") {
const body = await request.json()
const result = db
.query("INSERT INTO goods (title, stock) VALUES (?, ?) RETURNING *")
.get(body.title, body.stock ?? 0)
return Response.json(result, { status: 201 })
}
if (request.method === "GET" && url.pathname === "/goods") {
const rows = db.query("SELECT * FROM goods").all()
return Response.json(rows)
}
return new Response("Not Found", { status: 404 })
}
})
讲解:
- 请求方法 + 路径组合成路由:
POST /goods创建,GET /goods列表。 await request.json()解析请求体,然后参数化插入数据库。- 这 30 行代码就是一个可运行的商店 API:无框架、无 ORM、无额外依赖。
- 进阶:多表关联、迁移、连接池等场景再引入 Prisma/Drizzle 等 ORM,简单场景内置 SQLite 足够。
4. Bun.SQL:统一 SQL 客户端(Postgres 1.2+,MySQL 与 SQLite 1.3+)
bun:sqlite 是 SQLite 专属的同步 API;如果目标是 PostgreSQL 或 MySQL,用统一的 SQL 客户端——Postgres 支持自 Bun 1.2 起内置(sql 标签模板),1.3 起扩展为覆盖三类数据库的统一 API,连接串自动识别数据库类型,一套语法通吃三家:
// sql.ts —— import { SQL } from "bun",连接串决定驱动
import { SQL } from "bun"
const sql = new SQL("postgres://user:pass@localhost:5432/shop")
// MySQL:new SQL("mysql://user:pass@localhost:3306/shop")
// SQLite:new SQL("sqlite://app.db") 或 ":memory:"(内存库)
// 标签模板:${} 占位由驱动参数化,天然防注入
const goods = await sql`SELECT * FROM goods WHERE stock > ${0}`
console.log(goods)
await sql`INSERT INTO goods (title, stock) VALUES (${`T 恤`}, ${10})`
// 事务:transaction 回调内抛错自动回滚
await sql.begin(async tx => {
await tx`UPDATE goods SET stock = stock - 1 WHERE id = ${1}`
await tx`INSERT INTO orders (goods_id) VALUES (${1})`
})
讲解:
import { SQL } from "bun"一行拿到 Postgres/MySQL/SQLite 三种驱动,无需pg、mysql2或better-sqlite3——lockfile 直接瘦身。- 标签模板
sql`...`返回 Promise,${}里的值由驱动参数化;三条数据库同语法,换库只改连接串。 sql.begin开启事务,回调内用tx执行同事务语句,抛错自动回滚——与第 2 节 SQLite 事务语义一致,心智模型只有一套。- 选型口诀:单机小工具用
bun:sqlite(同步、简单、快);要连 Postgres/MySQL 或想保留换库自由,用统一SQL类。
5. 内置 Redis 客户端
import { redis } from "bun" // 默认实例:读取 REDIS_URL 环境变量(默认 redis://localhost:6379)
await redis.set("counter", 1)
await redis.incr("counter")
const value = await redis.get("counter")
console.log(value) // "2"
// 需要多个连接或自定义地址时,用 RedisClient 类
import { RedisClient } from "bun"
const client = new RedisClient("redis://localhost:6379")
await client.set("greeting", "你好")
console.log(await client.get("greeting"))
讲解:
- Bun 1.3+ 内置 Redis 客户端(
bun模块导出redis默认实例与RedisClient类),无需安装ioredis等第三方包;要求 Redis 服务器 7.2+。 - 连接是惰性的:首次执行命令才真正建连;断线自动指数退避重连。
- API 风格与 ioredis 高度一致:
set/get/incr都是 Promise,可用await;内存缓存、分布式锁、限流等场景可直接使用。 - 典型组合:SQLite/Postgres 存业务数据,Redis 存计数与热点缓存——商店首页的”浏览量”就该放 Redis,别每次写库。
6. 常见陷阱
- 拿
bun:sqlite的同步 API 阻塞事件循环:db.query(...).all()是同步的,大查询会卡住所有并发请求;数据量大时加LIMIT、走索引,或换统一的SQL异步客户端。 - SQL 字符串拼接:两种 API 都支持参数化(
?占位符与标签模板${}),拼接用户输入等于开门揖盗。 - Redis 当持久化存储:
set不带过期策略、不落盘配置时,重启即丢;缓存可丢,订单不可丢——数据分层要想清楚。 - 端口占用误以为代码错了:
EADDRINUSE是上一个bun server.ts还挂着;改端口或先结束旧进程。 - 忘了环境变量注入:
redis默认实例读REDIS_URL,生产环境忘配就静默连 localhost;关键连接用RedisClient显式传地址并在启动时ping验证。
7. 动手试试
- 给商店 API 增加
DELETE /goods/:id(路由表动态段 + 参数化 DELETE)。 - 用
bun:sqlite做一个”点击计数”页面:每次访问/counter把数字加 1 并返回,再改用 Redis 实现一遍,对比两种写法。 - 用 Docker 起一个 PostgreSQL(
docker run -d -p 5432:5432 -e POSTGRES_PASSWORD=dev123 postgres),把统一SQL类的连接串指向它,跑通sql.begin事务。 - 给
GET /goods加?minStock=过滤参数,体会查询参数 -> SQL 条件的安全写法。
8. 本篇小结
初学者要点:
- Bun.serve 起服务(路由表 + fetch 兜底)、bun:sqlite 存数据、统一
SQL连主流数据库、内置 Redis 做缓存——小项目一个运行时全搞定。 - SQL 永远用参数占位符:
?与标签模板${}都是防线,字符串拼接是红线。 - 数据分层:业务数据进数据库、计数缓存进 Redis、临时数据用内存。
进阶注意:
- 同步的
bun:sqlite与异步的统一SQL各有适用面;并发量上来后,“同步大查询阻塞事件循环”是最常见的性能坑。 - 这些内置能力覆盖”从零到能用”,复杂场景(迁移、连接池、读写分离)仍会回到专业 ORM 与托管数据库——内置件是起点,不是终点。