Bun.js + Elysia 框架实现基于 SQLITE3 的简单 CURD 后端服务

🤔 关于 Bun.js
为什么是 Bun.js?
以前我用 JavaScript 写后端,用的组合拳是 Node.js + Fastify + better-sqlite3,用起来还是非常顺手的。但是会出现一些问题:
- better-sqlite3 需要构建为
.node二进制文件,与操作系统和 CPU 架构绑定。例如,Windows 下生成的 .node 文件无法在 Linux 或 macOS 上运行; - 在 macOS 下构建上述文件时会出错(node 版本
22.x,better-sqlite3 版本11.x/12.x)。
Bun.js 自1.2.21版本开始自带 sqlite3 模块,非常方便👍,同时 Bun.js 带来了比 node.js 更高的性能,还自带绝绝子的包管理器(代替 pnpm)以及看不到车尾灯的构建速度(代替 webpack/vite 等),于是我决定投入 Bun.js 的怀抱。
还有一个重要的原因是,node.js 启用 module 模式后,引入其他js文件路径填写非常严格,比如:
// a.js
export const add = (x,y)=> x+y// b.js
import { add } from './a.js'
// 不能写成 import { add } from './a'
// 也不支持自动引入目录下的 index.js
// 上述都是 commonjs 模式下非常实用的引入机制😄
如何兼容 commonjs 引入机制
我们可以编写自定义 loader:
// commonjs-loader.js
import { register } from "node:module";
import { fileURLToPath } from 'url';
import { existsSync } from 'fs';
import { dirname, join } from 'path';// 拦截模块解析的钩子
async function resolve(specifier, context, defaultResolve) {// specifier 是导入路径(如 "./a")// context 包含父模块路径等信息const { parentURL } = context;// 仅处理相对路径(以 ./ 或 ../ 开头),避免影响第三方模块if (specifier.startsWith('./') || specifier.startsWith('../')) {// 将父模块的 URL 转为文件路径const parentPath = fileURLToPath(parentURL);const parentDir = dirname(parentPath);// 拼接可能的文件路径(尝试添加 .js)const candidatePath = join(parentDir, specifier + '.js');// 如果带 .js 的文件存在,则修改 specifier 为带后缀的路径if (existsSync(candidatePath)) {specifier += '.js';}// 如果 index.js 文件存在else if(existsSync(join(parentDir, specifier, "index.js"))){specifier += '/index.js'}}// 调用默认解析逻辑return defaultResolve(specifier, context, defaultResolve);
}// 注册当前模块为 loader
register(new URL(import.meta.url), { parentURL: import.meta.url });
export { resolve };
然后按照 node --import ./commonjs-loader.js src/app.js 的启动方式即可。
1.3 版本大更新
Bun.js 在 2025年10月10日发布了1.3.0版本,从 “高性能 JS 运行时” 升级为 “一站式全栈开发解决方案”,不仅原生支持前端开发全流程(热重载、打包构建),还新增了 MySQL 客户端、Redis 客户端等企业级工具,同时大幅提升 Node.js 兼容性。
这就意味着,我们可以使用 Bun.js 来开发前端项目了。
1.3.0 的小 BUG
2025-10-11 发布的1.3.0版本,在 windows 下无法正常执行脚本(package.json 定义),详见:Bun.js cannot run scripts correctly on Windows 11。

该问题在1.3.1中已修复👍。
与 Node.js 的兼容性
- Bun 明确表示其目标是成为 Node.js 的「可替换」运行时,并且在官网强调它在 “Node-style 模块解析、全局变量(例如
process,Buffer)及核心模块(如fs,path,http)” 方面已有广泛支持; - 对于 Node-API (原生扩展接口,即 N-API) 的支持也已经达到较高水平——文档提到 Bun 实现了约 95% 的 Node-API 接口,从而大部分用来构建 Node 原生模块(*.node 文件)在 Bun 上“开箱即用”;
- 在许多主流基于 Node 的项目/框架(比如 Express、Next.js)上,Bun 已经能较好运行,迁移成本低。
我原本的代码可以直接迁移到 Bun.js 环境下运行,十分省心😄。
我的开发方式
使用 Bun.js 作为包管理器及打包工具,按需配合 vite/webpack 完成前端开发。
🛴 CURD 应用
这是一个简单的用户管理演示,包含以下接口:
- /create:创建新用户
- /delete:删除指定用户
- /query?id=:查询指定用户
- /all:列出全部用户
import { Elysia } from "elysia";
import { Database } from "bun:sqlite";// 初始化 SQLite 数据库(文件自动创建)
const db = new Database("user.db");// 创建 user 表(若不存在)
db.run(`CREATE TABLE IF NOT EXISTS user (id INTEGER PRIMARY KEY AUTOINCREMENT,name TEXT NOT NULL,phone TEXT NOT NULL);
`);// 初始化 Elysia 应用
const app = new Elysia()// 创建用户.post("/create", async ({ body }) => {const { name, phone } = body;if (!name || !phone) {return { code: 400, msg: "缺少参数 name 或 phone" };}const stmt = db.prepare("INSERT INTO user (name, phone) VALUES (?, ?)");const result = stmt.run(name, phone);return { code: 200, msg: "创建成功", id: result.lastInsertRowid };})// 删除用户.get("/delete", ({ query }) => {const id = Number(query.id);if (!id) return { code: 400, msg: "缺少参数 id" };const stmt = db.prepare("DELETE FROM user WHERE id = ?");const result = stmt.run(id);if (result.changes === 0) {return { code: 404, msg: "未找到该用户" };}return { code: 200, msg: "删除成功" };})// 查询用户.get("/query", ({ query }) => {const id = Number(query.id);if (!id) return { code: 400, msg: "缺少参数 id" };const stmt = db.prepare("SELECT * FROM user WHERE id = ?");const user = stmt.get(id);if (!user) {return { code: 404, msg: "未找到该用户" };}return { code: 200, data: user };})// 列出所有用户.get("/list", () => {const stmt = db.prepare("SELECT * FROM user ORDER BY id DESC");const users = stmt.all();return { code: 200, data: users };})// 监听端口.listen(9000, v=>{console.debug("LISTEN CALLBACK", v)});console.log(`✅ Elysia app running at http://localhost:9000`);
Bun.js SQLite3 简单封装
import { Database } from 'bun:sqlite'
import logger from './common/logger'/**@type {Database} */
let db = undefinedexport const setupDB = ()=>{if(db == undefined){db = new Database("db.file")}
}/*** 遍历 tables 的定义语句进行建表* @param {Array<String>} tables - 建表语句合集*/
export const initDB = tables =>{setupDB()const regex = /CREATE TABLE IF NOT EXISTS\s+(\w+)\s*\(/ifor(let table of tables){let m = table.match(regex)if(m && m[1]){db.run(table)}}
}/*** 数据库执行方法枚举* @typedef {'run' | 'all' | 'get'} DBMethod*** @param {DBMethod} method* @param {String} sql* @param {...any} ps* @returns*/
const withDB = (method, sql, ...ps)=>{let stmt = db.prepare(sql)return stmt[method](...ps)
}export const exec = (sql, ...ps)=> withDB('run', sql, ...ps)export const query = (sql, ...ps)=> withDB('all', sql, ...ps)export const findByID = (table, id, idField='id')=> {return withDB('get', `SELECT * FROM ${table} WHERE ${idField}=?`, id)
}export const delByID = (table, id, idField='id')=> withDB('run', `DELETE FROM ${table} WHERE ${idField}=?`, id)export const findFirst = (sql, ...ps)=> withDB('get', sql, ...ps)/*** 获取指定条件的数据行数* @param {String} table* @param {String} condition* @param {...any} ps* @returns {Number}*/
export const count = (table, condition, ...ps)=> {let obj = withDB('get', `SELECT count(*) FROM ${table} WHERE ${condition}`, ...ps)return obj['count(*)']
}/**** @param {String} table* @param {Object} bean* @param {Array<String>} ignores*/
export const insertNew = (table, bean, ignores=[])=>{let fields = Object.keys(bean)if(ignores && ignores.length)fields = fields.filter(f=>!ignores.includes(f))return exec(`INSERT INTO ${table} (${fields.map(f=>f).join(",")}) VALUES (${fields.map(()=>"?").join(",")});`, fields.map(f=>bean[f]))
}
📚 附录
windows 下更新 Bun.js
# 官网是推荐使用 bun upgrade 升级
# 可是我在 windows 下通过 cmd 执行会出现长时间的卡顿而导致无法升级
# 最后还是通过重新安装的方式完成升级😔
powershell -c "irm bun.sh/install.ps1 | iex"
Bun.js 如何判断环境
await Bun.build({define:{"Bun.env.NODE_ENV": JSON.stringify("production"),"process.env.NODE_ENV": JSON.stringify("production")}
})
通过上述方案打包后的文件,可获取process.env.NODE_ENV的值用于判断当前运行环境。
打包
经过一番摸索,我整理了一份简单实用的打包脚本,仅供参考。
将下方代码保存到 build.js 文件,然后执行 bun build.js 即可。
# build.js
import { formatFileSize } from "./src/common/tool"
import pc from 'picocolors'const VERSION = ()=>{let now = new Datereturn `v${now.getUTCFullYear() - 2000}.${now.getUTCMonth() + 1}.${now.getUTCDate()}`
}const ENV = "production"const started = Date.now()
const result = await Bun.build({entrypoints:["./src/server.js"],minify: true,outdir:"./dist",naming:"[dir]/ai-naming.js",target: 'bun',env: 'disable',define:{"APP_VERSION": JSON.stringify(VERSION()),"Bun.env.NODE_ENV": JSON.stringify(ENV),"process.env.NODE_ENV": JSON.stringify(ENV)}
})if(!result.success){console.debug(`Build fail:`, result.logs)process.exit(-1)
}let cwd = process.cwd()console.debug(pc.green(`\n✅ 构建完成(env=${ENV}),耗时 ${Date.now()-started} ms\n`))
for(let item of result.outputs){console.debug(pc.cyan(`${item.path.replace(cwd, " ")}\t${item.hash}\t${formatFileSize(item.size)}`))
}

