Bun 1.3: Native SQLite Support, Hot Module Reloading, and ESM Enhancements
Bun 1.3 brings native SQLite integration, improved hot module reloading, and better ESM compatibility. Learn what's new and how to migrate your projects.
Bun 1.3: What’s New
Bun, the all-in-one JavaScript runtime, package manager, and bundler, has reached version 1.3 with significant upgrades aimed at making development faster and more seamless. This release focuses on three core areas: native SQLite database support, enhanced hot module reloading (HMR), and improved ECMAScript Module (ESM) compatibility.
If you’re building full-stack JavaScript applications, server-side tools, or exploring alternatives to Node.js and npm, this release deserves your attention. Bun 1.3 removes friction from common development workflows and adds native capabilities that previously required third-party packages.
Understanding Bun’s Position
Bun started as a bold alternative to Node.js, promising faster startup times, lower memory consumption, and a batteries-included experience (runtime + package manager + bundler + test runner). Over the past year, Bun has matured significantly, and 1.3 marks another step toward production readiness for mainstream use.
Unlike Node.js, which traditionally requires you to install separate tools (npm/yarn/pnpm, webpack/esbuild, Jest/Vitest), Bun bundles everything. This “one runtime to rule them all” philosophy simplifies the toolchain—especially useful for teams tired of JavaScript fatigue and dependency sprawl.
Native SQLite Support: A Game-Changer
Why This Matters
One of the most compelling additions in Bun 1.3 is native SQLite support via the bun:sqlite module. SQLite is the world’s most widely deployed database—embedded in browsers, mobile devices, and countless applications. Until now, using SQLite in Bun required either:
- Better-sqlite3 – a native Node.js binding (performance-focused but complex to install)
- sql.js – a JavaScript port (portable but slower)
- Deno’s sqlite3 – a Deno-specific wrapper
Bun’s native implementation is built directly into the runtime, offering both simplicity and performance.
Getting Started with bun:sqlite
Here’s a practical example:
import { Database } from "bun:sqlite";
// Open or create a database file
const db = new Database("app.db");
// Create a table
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
// Insert data with prepared statements
const insert = db.prepare(
"INSERT INTO users (name, email) VALUES (?, ?)"
);
insert.run("Alice", "[email protected]");
insert.run("Bob", "[email protected]");
// Query data
const query = db.prepare("SELECT * FROM users WHERE email = ?");
const user = query.get("[email protected]");
console.log(user);
// Output: { id: 1, name: 'Alice', email: '[email protected]', created_at: '2024-12-01 ...' }
// Batch operations for performance
const insertMany = db.prepare("INSERT INTO users (name, email) VALUES (?, ?)");
db.transaction(() => {
for (let i = 0; i < 1000; i++) {
insertMany.run(`User${i}`, `user${i}@example.com`);
}
})();
// Close the database
db.close();
Key Features
Prepared Statements: db.prepare() compiles SQL once and reuses it—essential for performance and security (protects against SQL injection).
Transactions: The db.transaction() wrapper ensures ACID compliance and significantly speeds up bulk inserts.
Type Safety (with TypeScript): Bun’s TypeScript integration means you get autocomplete and type hints without extra build steps.
interface User {
id: number;
name: string;
email: string;
created_at: string;
}
const query = db.prepare("SELECT * FROM users");
const users: User[] = query.all();
// TypeScript knows the shape of users
Building an API Server with SQLite
Here’s a complete example using Bun’s built-in HTTP server and SQLite:
import { Database } from "bun:sqlite";
const db = new Database("api.db");
// Initialize schema
db.exec(`
CREATE TABLE IF NOT EXISTS posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
const selectAll = db.prepare("SELECT * FROM posts");
const selectById = db.prepare("SELECT * FROM posts WHERE id = ?");
const insert = db.prepare("INSERT INTO posts (title, content) VALUES (?, ?)");
const deletePost = db.prepare("DELETE FROM posts WHERE id = ?");
const server = Bun.serve({
port: 3000,
async fetch(req) {
const url = new URL(req.url);
const path = url.pathname;
// GET /posts
if (path === "/posts" && req.method === "GET") {
const posts = selectAll.all();
return Response.json(posts);
}
// GET /posts/:id
const postMatch = path.match(/^\/posts\/(\d+)$/);
if (postMatch && req.method === "GET") {
const id = parseInt(postMatch[1]);
const post = selectById.get(id);
return post ? Response.json(post) : new Response("Not found", { status: 404 });
}
// POST /posts
if (path === "/posts" && req.method === "POST") {
try {
const body = await req.json();
const { title, content } = body;
insert.run(title, content);
return new Response("Created", { status: 201 });
} catch (e) {
return new Response("Invalid JSON", { status: 400 });
}
}
// DELETE /posts/:id
if (postMatch && req.method === "DELETE") {
const id = parseInt(postMatch[1]);
deletePost.run(id);
return new Response("Deleted", { status: 204 });
}
return new Response("Not found", { status: 404 });
},
});
console.log(`Server running at http://localhost:${server.port}`);
Run with bun run server.js and test using API Request Builder.
Enhanced Hot Module Reloading (HMR)
What Changed
Bun 1.3 significantly improves HMR reliability and speed. Previously, HMR could be unpredictable in edge cases—modules sometimes failed to reload, or the reload was delayed. The new implementation:
- Instant feedback: File changes trigger reloads in milliseconds
- Better error recovery: If a module has a syntax error, HMR doesn’t crash the dev server
- Full module graph support: Works reliably with circular dependencies and complex dependency trees
Using HMR in Development
Bun 1.3 ships with built-in HMR for web frameworks. Here’s an example with a simple HTTP server:
// app.js
export function render() {
return "<h1>Hello from Bun</h1>";
}
if (import.meta.hot) {
import.meta.hot.accept(() => {
console.log("Module reloaded!");
});
}
// server.js
import { render } from "./app.js";
const server = Bun.serve({
port: 3000,
fetch() {
return new Response(render(), { headers: { "Content-Type": "text/html" } });
},
});
console.log(`Dev server: http://localhost:3000`);
Run with bun --hot run server.js. Edit app.js and watch it reload instantly.
The import.meta.hot API lets you handle custom reload logic—useful for preserving application state across reloads.
Improved ESM Compatibility
What Was Fixed
ECMAScript Modules (ESM) are the standardized way to import/export code in JavaScript, but implementation varies across runtimes. Bun 1.3 addresses several compatibility issues:
- Dynamic imports with top-level await: Now works reliably in more contexts
- Circular dependency resolution: Better handling of modules that depend on each other
- Mixed CommonJS/ESM interop: Improved compatibility when using both module systems in the same project
Example: Top-Level Await
// config.js (Bun now reliably supports this)
const env = await fetch("https://api.example.com/config").then(r => r.json());
export const API_URL = env.api_url;
export const DB_HOST = env.db_host;
// main.js
import { API_URL } from "./config.js";
console.log("Connecting to", API_URL);
Previously, Bun sometimes struggled with top-level await in certain module graph patterns. This is now solid.
Step-by-Step Migration Guide
For Existing Node.js/Deno Projects
1. Install Bun (if you haven’t already):
curl -fsSL https://bun.sh/install | bash
2. Convert your project:
cd your-project
bun install # Converts package.json and installs dependencies
Bun reads your package.json and creates bun.lock (much faster than package-lock.json).
3. Update scripts in package.json:
{
"scripts": {
"dev": "bun --hot run src/server.js",
"build": "bun build ./src/index.js --outdir ./dist",
"test": "bun test",
"start": "bun run src/server.js"
}
}
4. Replace Node.js-specific dependencies:
If you were using better-sqlite3:
# Remove the old package
bun remove better-sqlite3
Update your code:
// Before (Node.js)
const Database = require("better-sqlite3");
const db = new Database("app.db");
// After (Bun 1.3)
import { Database } from "bun:sqlite";
const db = new Database("app.db");
5. Test thoroughly:
bun test
bun --hot run src/server.js # Start dev server
Compatibility Considerations
Bun 1.3 is production-ready for most use cases, but consider:
-
Native modules: If your project depends on native Node.js addons (like
bcryptwith native bindings), test carefully or use pure-JS alternatives - Missing Node APIs: Some obscure Node.js APIs aren’t implemented yet; check the Bun compatibility matrix
- Package manager differences: Bun’s lockfile format and dependency resolution differ slightly from npm/yarn
Common Pitfalls and Solutions
Issue: SQLite File Permissions
Problem: Multiple processes can’t write to the same SQLite database simultaneously.
Solution: Use WAL (Write-Ahead Logging) mode for better concurrency:
const db = new Database("app.db");
db.exec("PRAGMA journal_mode = WAL");
This allows readers and writers to work simultaneously (with limitations—check SQLite docs).
Issue: HMR Not Detecting Changes
Problem: File changes don’t trigger reloads.
Solution: Ensure you’re using --hot and that your file watcher supports your OS:
# Explicitly use polling for better compatibility
BUN_FILE_WATCHER=polling bun --hot run server.js
Issue: Memory Leaks with Large SQLite Datasets
Problem: Loading millions of rows into memory crashes your process.
Solution: Use streaming or pagination:
const pageSize = 100;
const page = db.prepare(
"SELECT * FROM large_table LIMIT ? OFFSET ?"
);
for (let i = 0; i < totalPages; i++) {
const rows = page.all(pageSize, i * pageSize);
processRows(rows);
}
Why Bun 1.3 Matters Now
Developer Experience
Bun eliminates tool fatigue. Instead of managing npm + webpack + Jest + multiple config files, you have one runtime that does everything—and does it fast.
Performance
- Startup time: ~10x faster than Node.js
- SQLite queries: Faster than better-sqlite3 due to tighter integration
- Bundle size: Smaller output with the native bundler
Full-Stack JavaScript
Native SQLite means you can build complete backend applications (API servers, CLI tools, database-backed services) without leaving JavaScript.
Testing Your Code
Use Bun’s built-in test runner:
// user.test.js
import { Database } from "bun:sqlite";
import { expect, test, beforeAll, afterAll } from "bun:test";
let db;
beforeAll(() => {
db = new Database(":memory:");
db.exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)");
});
afterAll(() => {
db.close();
});
test("insert and retrieve user", () => {
const insert = db.prepare("INSERT INTO users (name) VALUES (?)");
insert.run("Alice");
const query = db.prepare("SELECT * FROM users WHERE name = ?");
const user = query.get("Alice");
expect(user.name).toBe("Alice");
});
Run tests with bun test.
Debugging and Monitoring
For complex SQLite schemas, use JSON Formatter to inspect query results, or SQL Formatter to format complex queries before running them.
For API testing, API Request Builder lets you test your Bun-powered endpoints with custom headers and payloads.
Conclusion
Bun 1.3 is a significant milestone. Native SQLite support removes a major hurdle for backend JavaScript development. Enhanced HMR makes development faster and more enjoyable. Improved ESM compatibility aligns Bun closer to web standards.
If you’ve been curious about Bun but hesitant about production readiness, 1.3 is a good inflection point to revisit. The ecosystem is maturing, the runtime is stable, and the quality-of-life improvements are real.
Start small: use Bun for new projects or experiment in a side tool. The learning curve is minimal if you already know Node.js, and the payoff in developer experience is substantial.