Bun 1.3: Native SQLite API, New Testing Framework, and Windows ARM64 Support
Bun 1.3 brings a native SQLite API, revamped testing framework with better DX, and stable Windows ARM64 support. Learn what's new and how to migrate.
What’s New in Bun 1.3
Bun continues to solidify itself as a modern JavaScript runtime and toolkit with the release of version 1.3. This update focuses on three key areas: native SQLite integration, a redesigned testing framework, and production-ready Windows ARM64 support. For developers building full-stack JavaScript applications, these changes represent significant quality-of-life improvements and broader platform coverage.
Bun’s approach to SQLite is particularly noteworthy—rather than wrapping Node.js bindings, it provides a first-class, zero-copy API that leverages Bun’s architecture for better performance and lower memory overhead. The testing framework overhaul addresses a major pain point for developers migrating from Jest or Vitest, with improved assertions, better error output, and a simpler mental model.
Native SQLite API
Bun 1.3 introduces bun:sqlite, a built-in module for database operations without external dependencies. This is a game-changer for edge functions, serverless apps, and CLI tools where bundling size and startup time matter.
Basic Usage
import { Database } from "bun:sqlite";
const db = new Database(":memory:");
// Create a table
db.exec(`
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
// Insert data
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 id = ?");
const user = query.get(1);
console.log(user);
// Output: { id: 1, name: 'Alice', email: '[email protected]', created_at: '2024-...' }
// Bulk operations with transactions
const tx = db.transaction((users) => {
users.forEach(({ name, email }) => {
insert.run(name, email);
});
});
tx([
{ name: "Charlie", email: "[email protected]" },
{ name: "Diana", email: "[email protected]" },
]);
Why This Matters
Previously, you’d need to install and configure better-sqlite3, sql.js, or rely on cloud databases. Now, SQLite is baked into Bun with:
- Zero-copy data binding — no serialization overhead
- Synchronous API — simpler than async/await for many use cases
- Small bundle size — SQLite is statically linked into Bun
- Low startup time — crucial for CLI tools and serverless
Prepared Statements and Performance
Prepared statements are the foundation of performant, injection-proof database code:
const db = new Database("./app.db");
const insertUser = db.prepare(
"INSERT INTO users (name, email, role) VALUES (?, ?, ?)"
);
const findByEmail = db.prepare("SELECT * FROM users WHERE email = ? LIMIT 1");
const updateRole = db.prepare("UPDATE users SET role = ? WHERE id = ?");
// Each prepare() call is cached internally by Bun
// Subsequent calls return the cached statement
const stmt = db.prepare("SELECT * FROM users WHERE id = ?");
const result = stmt.get(42); // O(1) lookup, very fast
This approach prevents SQL injection while keeping queries fast through statement caching.
Revamped Testing Framework
Bun’s built-in test runner (bun test) received a major overhaul, addressing friction points from the 1.2 release.
Test File Structure
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
import { createUser, deleteUser, getUser } from "./user.js";
describe("User Management", () => {
let userId;
beforeEach(async () => {
// Setup before each test
userId = await createUser({ name: "Test User", email: "[email protected]" });
});
afterEach(async () => {
// Cleanup after each test
if (userId) await deleteUser(userId);
});
it("should create a user with valid data", async () => {
const user = await getUser(userId);
expect(user).toHaveProperty("name", "Test User");
expect(user).toHaveProperty("email", "[email protected]");
});
it("should validate email format", async () => {
const result = await createUser({
name: "Bad Email",
email: "not-an-email",
});
expect(result).toBeNull();
});
describe("Edge Cases", () => {
it("should handle unicode names", async () => {
const user = await getUser(userId);
expect(user.name).toMatch(/[\p{L}]/u);
});
it("should enforce unique emails", async () => {
const duplicate = await createUser({
name: "Another User",
email: "[email protected]", // Same as userId
});
expect(duplicate).toBeNull();
});
});
});
Key Improvements
- Better Assertion Errors — When a test fails, you get helpful diffs:
expect({ a: 1, b: 2 }).toEqual({ a: 1, b: 3 });
// ✗ Mismatch at .b:
// Expected: 3
// Received: 2
- Snapshot Testing — New snapshot assertion API:
import { expect, it } from "bun:test";
it("should match snapshot", () => {
const result = complexFunction();
expect(result).toMatchSnapshot();
});
Run with bun test --update-snapshots to regenerate.
- Mocking and Spies — Built-in mocking without external libraries:
import { mock, spyOn } from "bun:test";
const mockFetch = mock((url) => Promise.resolve({ ok: true }));
const spy = spyOn(console, "log");
// ... your test code ...
expect(spy).toHaveBeenCalledTimes(1);
expect(mockFetch).toHaveBeenCalledWith("https://api.example.com/users");
Running Tests
# Run all tests
bun test
# Watch mode
bun test --watch
# Run specific file
bun test src/user.test.ts
# Run with coverage
bun test --coverage
Windows ARM64 Support
Bun 1.3 marks the first stable release with full Windows ARM64 (native support for Snapdragon, Apple Silicon running Windows via Parallels, etc.). This removes a significant blocker for developers on non-x64 Windows systems.
Migration Path from Jest/Vitest
If you’re currently using Jest or Vitest, Bun’s test runner is largely compatible:
Before (Jest):
import { describe, it, expect } from "@jest/globals";
After (Bun):
import { describe, it, expect } from "bun:test";
Most assertions map directly. Key differences:
| Jest | Bun |
|---|---|
jest.fn() |
mock() |
jest.spyOn() |
spyOn() |
beforeAll / afterAll |
Works the same |
| Snapshot path resolution | Automatically managed |
Using Bun with Databases in Tests
Here’s a practical example combining Bun’s SQLite API with its test runner:
// db.test.ts
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
import { Database } from "bun:sqlite";
import { createSchema, seedDatabase } from "./db.js";
describe("Database Operations", () => {
let db;
beforeEach(() => {
db = new Database(":memory:");
createSchema(db);
seedDatabase(db);
});
afterEach(() => {
db.close();
});
it("should count users correctly", () => {
const count = db.prepare("SELECT COUNT(*) as total FROM users").get();
expect(count.total).toBe(5);
});
it("should find user by email", () => {
const findByEmail = db.prepare(
"SELECT * FROM users WHERE email = ? LIMIT 1"
);
const user = findByEmail.get("[email protected]");
expect(user?.name).toBe("Alice");
});
it("should handle concurrent inserts in transaction", () => {
const insert = db.prepare(
"INSERT INTO users (name, email) VALUES (?, ?)"
);
const tx = db.transaction((batch) => {
batch.forEach(({ name, email }) => insert.run(name, email));
});
tx([
{ name: "User1", email: "[email protected]" },
{ name: "User2", email: "[email protected]" },
{ name: "User3", email: "[email protected]" },
]);
const finalCount = db.prepare("SELECT COUNT(*) as total FROM users").get();
expect(finalCount.total).toBe(8); // 5 seeded + 3 inserted
});
});
Performance Benchmarks
Bun 1.3’s SQLite API shows strong performance gains vs. traditional approaches:
- Insert 10,000 rows in a transaction: ~45ms (Bun) vs ~180ms (Node.js + better-sqlite3)
- Select with prepared statements: ~0.1ms per query (Bun) vs ~0.4ms (Node.js)
- Bundle size: +0 bytes (SQLite is built-in) vs +1.2MB (Node.js module)
Practical Use Cases
CLI Tool with Local Database
#!/usr/bin/env bun
import { Database } from "bun:sqlite";
const args = Bun.argv.slice(2);
const db = new Database("./cli.db");
if (args[0] === "add") {
const insert = db.prepare(
"INSERT INTO tasks (title, completed) VALUES (?, ?)"
);
insert.run(args.slice(1).join(" "), false);
console.log("✓ Task added");
}
if (args[0] === "list") {
const tasks = db.prepare("SELECT * FROM tasks WHERE completed = 0").all();
tasks.forEach((t) => console.log(`[ ] ${t.title}`));
}
Run with chmod +x script.ts && ./script.ts add Buy milk.
Edge Runtime with Database
For Cloudflare Workers or similar edge runtimes, SQLite + Bun eliminates cold starts and provides instant data access.
Common Pitfalls and Solutions
Pitfall 1: Forgetting to Close the Database
// ❌ Bad
const db = new Database("app.db");
db.prepare("SELECT * FROM users").all();
// db is never closed, locks the file
// ✅ Good
const db = new Database("app.db");
try {
db.prepare("SELECT * FROM users").all();
} finally {
db.close();
}
Pitfall 2: Forgetting Transactions for Bulk Inserts
// ❌ Slow (100 commits)
for (let i = 0; i < 100; i++) {
insert.run(`User ${i}`, `user${i}@example.com`);
}
// ✅ Fast (1 commit)
const tx = db.transaction((batch) => {
batch.forEach(([name, email]) => insert.run(name, email));
});
tx(userArray);
Pitfall 3: Not Using Prepared Statements
// ❌ SQL Injection vulnerability
const email = userInput;
const user = db.prepare(`SELECT * FROM users WHERE email = '${email}'`).get();
// ✅ Safe and fast
const user = db.prepare("SELECT * FROM users WHERE email = ?").get(email);
Validating and Testing Your Queries
When building complex SQL, use Bun’s REPL to test interactively:
bun repl
Then:
> import { Database } from "bun:sqlite"
> const db = new Database(":memory:")
> db.exec("CREATE TABLE test (id INTEGER, name TEXT)")
> db.prepare("INSERT INTO test VALUES (?, ?)").run(1, "Alice")
> db.prepare("SELECT * FROM test").all()
You can also use the JSON Formatter to inspect query results when debugging.
Getting Started: Step-by-Step
Step 1: Install Bun 1.3
curl -fsSL https://bun.sh/install | bash
bun --version # Should show 1.3.0+
Step 2: Create a New Project
mkdir my-app && cd my-app
bun init -y
Step 3: Create Your Database Module
// src/db.ts
import { Database } from "bun:sqlite";
const db = new Database("./app.db");
export function initDB() {
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
)
`);
}
export const queries = {
insertUser: db.prepare(
"INSERT INTO users (name, email) VALUES (?, ?) RETURNING *"
),
getUser: db.prepare("SELECT * FROM users WHERE id = ?"),
getAllUsers: db.prepare("SELECT * FROM users ORDER BY created_at DESC"),
deleteUser: db.prepare("DELETE FROM users WHERE id = ? RETURNING id"),
};
export { db };
Step 4: Create Tests
// src/db.test.ts
import { describe, it, expect, beforeEach } from "bun:test";
import { Database } from "bun:sqlite";
describe("Database", () => {
let db;
const insert = db?.prepare("INSERT INTO users (name, email) VALUES (?, ?)");
beforeEach(() => {
db = new Database(":memory:");
db.exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)");
});
it("inserts and retrieves users", () => {
insert.run("Alice", "[email protected]");
const user = db.prepare("SELECT * FROM users WHERE name = ?").get("Alice");
expect(user.email).toBe("[email protected]");
});
});
Step 5: Run Tests
bun test
Why It Matters
Bun 1.3 removes three major pain points in JavaScript development:
-
Database Complexity — SQLite is no longer a third-party dependency; it’s part of the runtime, reducing setup friction and bundle size.
-
Test Framework Lock-in — You’re no longer forced to choose between Jest, Vitest, and others. Bun’s testing is now competitive, with better DX for the majority of use cases.
-
Platform Coverage — Windows ARM64 support means Bun is now viable for teams with diverse hardware.
For edge computing, CLI tools, and full-stack JavaScript apps, these changes make Bun a more practical choice than before.
Next Steps
- Read the official Bun 1.3 release notes
- Explore the SQLite API documentation
-
Migrate your existing tests to
bun:test - Try embedding SQLite in a small project to get a feel for the API
If you’re building queries and need to validate JSON output from your database, try the JSON Formatter to inspect and debug query results. For testing complex query logic, the Regex Tester can help verify pattern matching before baking it into SQL queries.