The Bun Runtime: A Fast All-in-One JavaScript Toolkit
Overview
The Bun runtime is a fast, all-in-one replacement for Node.js. It runs JavaScript and TypeScript directly without a build step, ships with a built-in package manager, bundler, and test runner, and starts up noticeably faster than Node.js. The Bun runtime aims for Node.js API compatibility so existing code often runs with minimal changes.
Running code with Bun
Running code with Bun is straightforward:
bun run index.js
bun run index.ts
bun run index.tsx
Bun’s run command handles .js, .ts, and .tsx files the same way. Bun natively understands TypeScript and JSX, so there is no transpilation step required. You can place a TypeScript file next to a JavaScript file and run both with the same command — Bun figures out the module type from the extension:
// greeter.ts
function greet(name: string): string {
return `Hello, ${name}!`;
}
console.log(greeet("Bun"));
The output shows Bun executes TypeScript without any configuration file. Notice the typo in greeet; Bun runs the file anyway and lets the runtime error surface naturally, just like Node.js would with plain JavaScript. The compiler catches type errors at startup but does not prevent execution of a file that has unresolved references.
bun run greeter.ts
# Hello, Bun!
Bun infers the module system from the file extension and the nearest package.json. TypeScript files with imports need "type": "module" in package.json or the .mts extension, matching Node.js conventions.
You can also execute files directly without the run subcommand:
bun index.ts
Bun treats .ts files as first-class citizens, so bun index.ts works the same as bun run index.ts. This shorthand is convenient for quick scripts and one-off utilities. The same convention applies to .tsx and .jsx files, making React and Preact projects start without a separate build tool.
Built-in HTTP Server
Bun has a fast HTTP server built in. Bun.serve() takes a handlers object:
Bun.serve({
port: 3000,
fetch(req) {
const url = new URL(req.url);
if (url.pathname === "/") {
return new Response("Welcome to Bun!");
}
if (url.pathname === "/api/users") {
return Response.json({ users: ["alice", "bob"] });
}
return new Response("Not found", { status: 404 });
},
});
console.log("Server running at http://localhost:3000");
This server is production-grade: Bun.serve() uses the same HTTP engine as Bun’s own internals. Route matching happens in the fetch handler, so you can build a full REST API without Express or any other framework. Start it with bun run server.ts and the server is ready immediately.
Reading Files
Bun.file() creates a lazy file reader:
const file = Bun.file("data.json");
const contents = await file.text(); // string
const json = await file.json(); // parsed JSON
const buffer = await file.arrayBuffer(); // raw bytes
The file is not read until you call the accessor method. This lazy evaluation means you can create Bun.file() references without triggering I/O, which is useful when building file-aware middleware. Each accessor method — .text(), .json(), .arrayBuffer() — reads the file independently, so you can call multiple accessors on the same BunFile reference without re-opening the handle. This works well for serving static assets:
Bun.serve({
fetch(req) {
const url = new URL(req.url);
const file = Bun.file(`./public${url.pathname}`);
if (await file.exists()) {
return new Response(file);
}
return new Response("Not found", { status: 404 });
},
});
The Bun.file() reference returned to new Response() streams the file directly to the client. Bun handles the content type detection from the file extension, so you do not need to set headers manually unless you want to override them.
Bun.write for fast file output
const content = "Hello, Bun file writing!";
// Write string to file
await Bun.write("output.txt", content);
// Or use the file writer
const file = Bun.file("data.json");
await Bun.write(file, JSON.stringify({ hello: "world" }));
Bun.write() accepts strings, Buffer objects, BunFile references, Response bodies, and Blob instances. The method detects the target type and handles encoding automatically, which removes the boilerplate of choosing between writeFile and writeFileSync in Node.js. The method detects the target type and handles encoding automatically, which removes the boilerplate of choosing between writeFile and writeFileSync in Node.js.
Environment Variables
Load .env files automatically:
# .env
DATABASE_URL=postgres://localhost/mydb
PORT=3000
Bun loads .env files automatically at startup, before any module code runs. The variables appear in process.env exactly as they would with Node.js and the dotenv package. You can reference them directly without any import statement or setup function call.
// Access like Node.js process.env
console.log(process.env.DATABASE_URL);
console.log(process.env.PORT);
Bun reads .env files at startup and populates process.env, similar to how Node.js provides process.env as described in the process object reference. No additional package like dotenv is needed. If a .env file exists in the working directory, its values are available immediately without any import or configuration call. Bun also supports .env.local and .env.production following the same precedence rules as other tools in the ecosystem.
Hot Reload
Bun watches for file changes and restarts automatically when running with --watch:
bun --watch server.ts
Changes to .ts, .tsx, .js, and .json files trigger a restart. This replaces nodemon and similar tools. Bun watches the file tree and only restarts when a source file actually changes, not when log files or build output appear.
Running Scripts from package.json
Bun runs package.json scripts just like npm:
bun run dev
bun run build
bun run test
Bun runs package.json scripts the same way npm does, so existing projects with dev, build, and test scripts work without changes. The main difference is speed — script execution starts noticeably faster.
If you have both a dev script and a dev.ts file, bun run dev prioritizes the script defined in package.json:
{
"scripts": {
"dev": "bun --watch server.ts"
}
}
Bun prioritizes package.json scripts over files with the same name, so bun run dev runs the script even if dev.ts exists in the directory. This keeps the workflow familiar for teams migrating from npm or yarn, and it means existing package.json files work without modification.
Testing with Bun
Bun ships a built-in test runner that is Jest-compatible:
// math.test.ts
import { describe, test, expect } from "bun:test";
function add(a: number, b: number): number {
return a + b;
}
describe("add", () => {
test("adds two numbers", () => {
expect(add(2, 3)).toBe(5);
});
test("handles negative numbers", () => {
expect(add(-1, 1)).toBe(0);
});
});
The test runner API mirrors Jest closely: describe, test, expect, and lifecycle hooks work the same way. If your project already uses Jest, moving to bun:test requires almost no code changes beyond the import path. The runner discovers test files automatically and reports results in the same format. If your project already uses Jest, moving to bun:test requires almost no code changes beyond the import path.
Run tests:
bun test
bun test --coverage
The test runner supports describe, test, it, beforeEach, afterEach, and the full expect API from Jest. No configuration file is required; bun test discovers test files following the same conventions as Jest (*.test.ts, *.spec.ts).
Built-in SQLite
Bun includes a fast SQLite driver:
import { Database } from "bun:sqlite";
const db = new Database("app.db");
// Create a table
db.run(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE
)
`);
// Insert data
db.run("INSERT INTO users (name, email) VALUES (?, ?)", "Alice", "alice@example.com");
// Query data
const users = db.query("SELECT * FROM users WHERE name = ?").all("Alice");
console.log(users);
The built-in SQLite driver is synchronous by default, which simplifies database calls in scripts and CLI tools. For concurrent workloads, the same driver works inside async functions without blocking the event loop. Prepared statements are also supported through the same query interface.
Bun vs Node.js compatibility
Bun aims to be a drop-in replacement for Node.js. Most npm packages work, and Node.js built-in modules (fs, path, crypto, Buffer, process, stream) are available:
import { readFileSync } from "fs";
import { join } from "path";
const config = readFileSync(join(__dirname, "config.json"), "utf-8");
Where Bun diverges from Node.js:
| Feature | Bun | Node.js |
|---|---|---|
| TypeScript | Native, no config | Requires ts-node or compilation |
| JSX | Native | Requires a build step |
| npm packages | Works | Works |
node_modules resolution | Fast | Standard |
| Built-in test runner | Jest-compatible | External (Jest/Vitest) |
| Hot reload | --watch flag | nodemon or similar |
| Startup time | ~3x faster | Slower |
Using Bun’s package manager
Bun’s package manager is significantly faster than npm or yarn:
bun install
bun add express
bun add -d typescript @types/node
bun remove lodash
bun install reads from package.json and bun.lockb, Bun’s binary lockfile format. If no bun.lockb exists, Bun falls back to package-lock.json for compatibility with existing projects. Install times are noticeably faster than npm because Bun resolves and fetches packages in parallel.
Common Flags
bun run --watch server.ts # Hot reload
bun run --cwd /path/to/project # Change working directory
bun build ./entry.ts --outdir ./dist # Bundle
bun test --reporter=verbose # Verbose test output
These flags cover the most common workflow adjustments. --watch for development, --cwd for monorepo navigation, and --outdir for production builds. Bun’s bundler handles tree-shaking and minification out of the box.
Everyday developer experience
Runtime details matter most in the daily rhythm of work. If starting the app is quick, making a small change feels less risky. If tests run fast, you are more likely to run them before moving on. If package installs finish quickly, setting up a new branch or rebuilding after a clean checkout feels less like a chore. Bun stands out when those little moments add up.
Judging Bun on your own workflow instead of a spec sheet alone is more useful than comparing feature checklists. If it makes your local loop calmer and your commands simpler, that is meaningful even if another runtime is still the safer fit for certain projects. The best tool is the one the team will actually enjoy using on an ordinary day.
Where Bun fits well
Bun is a good fit when you want one toolchain for running, testing, bundling, and installing packages. That makes it useful for small services, prototypes, and teams that want less time spent wiring together separate tools. The runtime feels especially natural for projects that already live in TypeScript, because you can keep the source files close to how they ship. If the project changes quickly and you would rather spend time on app code than on build plumbing, Bun can save a noticeable amount of setup.
That said, the right choice still depends on the project. If you rely on a package or platform feature that Bun does not yet handle the way you need, the more conservative path may be easier. Think of Bun as a practical option rather than a mandate. The best signal is whether it removes friction for the work in front of you. When it does, the speed of the runtime and the small amount of ceremony around it are hard to ignore.
Runtime tradeoffs
The main tradeoff with Bun is that the whole stack moves together. That is a strength when you want fewer moving parts, but it also means you should be comfortable with how Bun handles compatibility and package resolution. For many apps that is a fine bargain. For others, especially ones with old dependencies or unusual deployment constraints, it is worth checking a few critical code paths before committing to it across the whole project. A small test run can answer a lot before you switch the main branch over.
Team habits also matter when choosing a runtime. Some teams prefer a runtime that behaves almost exactly like Node.js and are willing to trade startup speed for that familiarity. Others want faster local loops and do not mind a little adaptation. Bun works best when the whole team understands why it is being used and what parts of the stack it replaces. That shared understanding keeps adoption smooth and prevents confusion when a package or command behaves a little differently than expected.
Practical adoption
If you are introducing Bun into an existing codebase, start with one low-risk path. That could mean running tests, serving a small local app, or using the package manager on a side project. Once that is stable, you can move more of the workflow over. This staged approach helps you notice where Bun feels better and where it needs extra attention. It also gives the team a chance to update scripts and docs without a big one-time migration.
The biggest day-to-day gain usually comes from shorter feedback loops. Starting a server, running a test file, or installing dependencies can feel much quicker, and that changes how often people check their work. When the tools get out of the way, you spend more time on the actual application. That is the real value here: not novelty, but fewer interruptions between an idea and a working result.
Use Bun where the workflow fits
Bun is strongest when it removes friction from the daily loop. If the runtime can start the app quickly, run tests without extra setup, and keep package management simple, that speed shows up in how often people check their work. It is especially attractive for small services and TypeScript-first projects where one tool can cover several steps without much ceremony.
That does not mean every codebase should switch immediately. The useful question is whether Bun fits the team’s habits and the project’s dependencies. If the answer is yes, the payoff is fewer moving parts and a quicker path from edit to run. If the answer is no, the conservative choice may still be the better one. The value comes from reducing the work in front of you, not from adopting a runtime for its own sake.
See Also
- /guides/javascript-event-loop/ explains how JavaScript runtimes handle async operations, relevant to Bun’s single-threaded model
- /guides/javascript-vite-guide/ covers Vite as an alternative bundler and dev server, sometimes used alongside Bun
- /tutorials/node-js-essentials/node-getting-started/ walks through getting started with Node.js, for context on the ecosystem Bun replaces