# Bun: a faster JavaScript runtime

> Learn Bun from scratch: install the runtime, run TypeScript, manage packages, create an HTTP server, write tests, bundle code, and use SQLite.

Author: [Flavio Copes](https://flaviocopes.com/about/) | Published: 2026-08-07 | Updated: 2026-08-07 | Topics: [JavaScript](https://flaviocopes.com/tags/js/) | Canonical: https://flaviocopes.com/bun/

Bun is a fast JavaScript runtime, like [Node.js](https://flaviocopes.com/nodejs/) and [Deno](https://flaviocopes.com/deno/). You can use it to run JavaScript and [TypeScript](https://flaviocopes.com/typescript/) outside the browser.

But calling Bun a runtime only tells part of the story.

The same `bun` command is also a package manager, a test runner, a bundler, and a project runner. It can read files, start an HTTP server, connect to SQLite, and turn a TypeScript program into a standalone executable.

You get all of this from one binary.

This is what first made Bun interesting to me. A lot of JavaScript development consists of choosing tools, installing them, and making them work together. Bun gives you good defaults before you install anything.

In this tutorial we'll install Bun, create a project, and explore those tools one at a time.

## What is a JavaScript runtime?

JavaScript started as a language for web browsers.

The browser gives JavaScript an engine and a collection of useful APIs. The engine runs the language. The APIs let your code work with things like the page, timers, network requests, and storage.

A server-side JavaScript runtime does something similar outside the browser. It gives JavaScript an engine plus APIs for files, processes, networking, and other operating system features.

Node.js is the best-known example. Bun is a newer alternative.

Bun uses JavaScriptCore, the JavaScript engine used by Safari. Node.js uses V8, the engine used by Chrome. Bun's runtime and tooling are mostly written in Zig, with a strong focus on startup time and performance.

That different foundation is one reason Bun can be very fast. But speed is not the only reason to use it.

The bigger difference is the amount of tooling included with the runtime.

## What comes with Bun?

Installing Bun gives you several tools:

- a **JavaScript and TypeScript runtime**
- an **npm-compatible package manager**
- a **package script runner**
- a **test runner** with a Jest-like API
- a **bundler** for browser and server code
- an **HTTP server**
- built-in APIs for files, SQLite, password hashing, SQL, Redis, and more

With Node.js, you normally combine Node with npm, a TypeScript runner, a test library, a watcher, and a bundler.

That is not a criticism of Node. Its ecosystem is enormous, stable, and proven. Bun makes a different tradeoff: it puts many common tools in one place.

The current Bun 1.3 series also includes a full-stack development server and more built-in database clients. Bun is no longer just a faster way to run a script. It wants to cover most of the development workflow.

## Installing Bun

On macOS and Linux, the simplest installation method is Bun's official script:

```bash
curl -fsSL https://bun.sh/install | bash
```

If you use Homebrew on macOS, you can install it this way:

```bash
brew tap oven-sh/bun
brew install bun
```

On Windows, open PowerShell and run:

```powershell
powershell -c "irm bun.sh/install.ps1 | iex"
```

You can also install Bun through npm if Node.js is already installed:

```bash
npm install -g bun
```

Now verify the installation:

```bash
bun --version
```

You can also print the exact source revision:

```bash
bun --revision
```

To upgrade Bun later, run:

```bash
bun upgrade
```

## Running your first JavaScript program

Let's start with the smallest possible program.

Create a file named `hello.js`:

```js
console.log('Hello from Bun')
```

Run it with Bun:

```bash
bun hello.js
```

You can include the optional `run` word too:

```bash
bun run hello.js
```

Both commands do the same thing.

This is the Bun equivalent of running `node hello.js`. Bun loads the file, executes the JavaScript, prints the message, and exits.

You can also evaluate a short expression without creating a file:

```bash
bun -e "console.log(2 + 2)"
```

This is useful for quick experiments.

## Creating a Bun project

A real project needs a `package.json`, TypeScript settings, and a few other files. Bun can create them for you.

Make a new folder and run `bun init`:

```bash
mkdir bun-demo
cd bun-demo
bun init
```

Bun asks which kind of project you want to create. Pick the blank project for this tutorial.

The generated `package.json` is small. It identifies the project and points to its main file. Bun also creates a TypeScript configuration and installs Bun's type definitions.

Run the generated entry point:

```bash
bun run index.ts
```

This is already a TypeScript project. We did not install a separate TypeScript runner.

## Running TypeScript directly

Bun can execute `.ts` and `.tsx` files without a build step.

Create `greet.ts`:

```ts
function greet(name: string) {
  return `Hello ${name}`
}

console.log(greet('Flavio'))
```

Run it:

```bash
bun greet.ts
```

Bun removes the TypeScript syntax and runs the resulting JavaScript.

There is an important detail here: **Bun does not type-check your program before running it**.

For example, this file can still start:

```ts
const age: number = 'twenty'

console.log(age)
```

Bun's job is to transpile and execute it. Your editor may show the type error, but Bun does not stop the program because of it.

Use the TypeScript compiler when you want a separate type-checking step:

```bash
bun add -d typescript @types/bun
bunx tsc --noEmit
```

My advice is to run that check in your test or build process. Fast execution is useful, but it should not replace type checking.

## ES modules and CommonJS

JavaScript has two module systems in common use.

New projects normally use ES modules with `import` and `export`:

```js
import { join } from 'node:path'

console.log(join('posts', 'bun.md'))
```

Older Node.js projects often use CommonJS and `require()`:

```js
const { join } = require('node:path')

console.log(join('posts', 'bun.md'))
```

Bun supports both. It can also load many CommonJS packages from ES modules, which helps when you use the existing npm ecosystem.

I still recommend ES modules for new projects. They are the JavaScript standard and work in browsers too.

## Installing npm packages

Bun is compatible with the [npm](https://flaviocopes.com/npm/) package ecosystem. You still use `package.json`, `node_modules`, semantic versions, and the npm registry.

Install all dependencies listed in `package.json`:

```bash
bun install
```

Add a package:

```bash
bun add hono
```

Add a development dependency:

```bash
bun add -d typescript
```

Remove a package:

```bash
bun remove hono
```

Update one package:

```bash
bun update hono
```

Or update all packages:

```bash
bun update
```

The commands are different from npm, but the files and packages are familiar.

### The Bun lockfile

The first `bun install` creates `bun.lock`.

This file records the exact dependency versions Bun resolved. Commit it to Git. Your computer, your teammates, and your deployment system should all install the same versions.

In CI, use:

```bash
bun ci
```

This is equivalent to `bun install --frozen-lockfile`. It fails if `package.json` and `bun.lock` disagree instead of silently changing the lockfile.

When you run `bun install` in an existing npm, Yarn, or pnpm project, Bun can migrate the existing lockfile into `bun.lock`. It preserves the old file, so you can review the result before deleting anything.

### A note about install scripts

Some npm packages run code during installation. Native packages often use a `postinstall` script to download or build a binary.

Bun does not run arbitrary lifecycle scripts from dependencies by default. This is a security feature, but it can surprise you when a package depends on that script.

If you trust the package, allow it explicitly:

```bash
bun pm trust package-name
```

Then install again. Do not trust a package only to make an error disappear. Check what its install script does first.

## Running package.json scripts

Suppose your `package.json` contains these scripts:

```json
{
  "scripts": {
    "dev": "bun server.ts",
    "test": "bun test",
    "typecheck": "tsc --noEmit"
  }
}
```

Run one with `bun run`:

```bash
bun run dev
```

I prefer keeping `run`. `bun run dev` makes it clear that `dev` comes from `package.json`.

This distinction matters for built-in commands. `bun run test` runs the `test` script from `package.json`, while `bun test` starts Bun's test runner directly. In our example they lead to the same command, but that is not always true.

Bun also adds local package executables from `node_modules/.bin` to the script path. You do not need to install those tools globally.

## Running a package with bunx

`bunx` runs a package executable without installing it globally. It plays the same role as `npx`.

For example:

```bash
bunx cowsay 'Hello from Bun'
```

This downloads the package if needed, caches it, and runs its command.

You will often use `bunx` for project generators and one-off tools:

```bash
bunx tsc --noEmit
```

If a tool belongs to your project, add it as a development dependency instead. That gives everyone the same version.

## Environment variables without dotenv

Bun automatically reads `.env` files.

Create a `.env` file:

```text
APP_NAME=Bun demo
PORT=3000
```

Read the values through `process.env`:

```ts
console.log(process.env.APP_NAME)
console.log(process.env.PORT)
```

Or use `Bun.env`:

```ts
console.log(Bun.env.APP_NAME)
```

Bun loads `.env`, an environment-specific file such as `.env.development`, and `.env.local`. Later files take priority over earlier files.

This means you usually do not need the `dotenv` package.

Be careful with secrets. Add `.env.local` to `.gitignore`, and use your hosting provider's secret storage in production.

## Watch mode and hot reload

During development, you want the program to react when a file changes.

Bun has two modes for this.

Use `--watch` to restart the process:

```bash
bun --watch server.ts
```

Bun follows the imported files. When one changes, it starts the process again.

Use `--hot` to reload modules without restarting the whole process:

```bash
bun --hot server.ts
```

Hot reload keeps global state and can keep an HTTP server alive. That can make server development feel almost instant.

Start with `--watch`. A complete restart is easier to reason about. Move to `--hot` when keeping the process alive gives you a real benefit.

Notice where the flag goes when you run a package script:

```bash
bun --watch run dev
```

Bun flags go immediately after `bun`. A flag at the end may be passed to the script instead.

## Using Web APIs in Bun

Bun implements many APIs you already know from browsers.

You can make an HTTP request with [`fetch()`](https://flaviocopes.com/fetch-api/):

```ts
const response = await fetch('https://api.github.com/users/flaviocopes')
const user = await response.json()

console.log(user.name)
```

The response is a standard `Response` object. You can also use `Request`, `Headers`, `URL`, `Blob`, `FormData`, and Web Streams.

This makes code easier to move between runtimes. It also explains the shape of Bun's HTTP server: a request comes in, and your handler returns a `Response`.

## Reading and writing files

The `Bun.file()` API gives you a lazy reference to a file.

Read `package.json` as text:

```ts
const file = Bun.file('package.json')
const text = await file.text()

console.log(text)
```

You can parse JSON directly:

```ts
const file = Bun.file('package.json')
const packageJson = await file.json()

console.log(packageJson.name)
```

Write a file with `Bun.write()`:

```ts
await Bun.write('message.txt', 'Hello from Bun')
```

Copy a file by passing one `BunFile` to `Bun.write()`:

```ts
const source = Bun.file('message.txt')

await Bun.write('message-copy.txt', source)
```

`Bun.file()` does not read the whole file when you create the reference. Bun waits until you ask for its contents or send it somewhere.

You can still use Node's `node:fs` APIs. They are useful for operations such as creating directories or reading a directory:

```ts
import { readdir } from 'node:fs/promises'

const files = await readdir('.')
console.log(files)
```

## Creating an HTTP server

Bun includes an HTTP server, so we can build a small API without installing Express.

Create `server.ts`:

```ts
const server = Bun.serve({
  port: 3000,
  fetch() {
    return new Response('Hello from Bun')
  },
})

console.log(`Listening on ${server.url}`)
```

Run it:

```bash
bun server.ts
```

Open `http://localhost:3000` in your browser.

The `fetch()` handler runs for every request. It returns the same standard `Response` object used by the browser Fetch API and Cloudflare Workers.

### Adding routes

You can define routes directly inside `Bun.serve()`:

```ts
const server = Bun.serve({
  port: 3000,
  routes: {
    '/': new Response('Home'),

    '/api/status': Response.json({ ok: true }),

    '/hello/:name': request => {
      return new Response(`Hello ${request.params.name}`)
    },
  },

  fetch() {
    return new Response('Not found', { status: 404 })
  },
})

console.log(`Listening on ${server.url}`)
```

A request to `/hello/Flavio` returns `Hello Flavio`.

The final `fetch()` handler is the fallback. Bun calls it when no route matches.

### Handling different HTTP methods

A route can have a different handler for each HTTP method:

```ts
const notes = ['Learn Bun']

Bun.serve({
  port: 3000,
  routes: {
    '/api/notes': {
      GET: () => Response.json(notes),

      POST: async request => {
        const body = await request.json()
        notes.push(body.text)

        return Response.json(body, { status: 201 })
      },
    },
  },
})
```

Test the GET route:

```bash
curl http://localhost:3000/api/notes
```

Create a note with POST:

```bash
curl -X POST http://localhost:3000/api/notes \
  -H 'Content-Type: application/json' \
  -d '{"text":"Build something"}'
```

This example keeps the notes in memory. They disappear when the server restarts, but the request and response flow is real.

For bigger applications, I like adding a framework such as Hono. Bun gives you the runtime and server. Hono adds a clean layer for routing, middleware, validation, and application structure.

## Testing with bun:test

Bun includes a test runner with an API inspired by Jest.

Create `math.ts`:

```ts
export function add(a: number, b: number) {
  return a + b
}
```

Now create `math.test.ts`:

```ts
import { expect, test } from 'bun:test'
import { add } from './math'

test('adds two numbers', () => {
  expect(add(2, 3)).toBe(5)
})
```

Run the test:

```bash
bun test
```

Bun finds files with names such as `*.test.ts`, `*.spec.ts`, and their JavaScript or JSX equivalents.

Run only tests whose path contains `math`:

```bash
bun test math
```

Run tests again when files change:

```bash
bun --watch test
```

The test runner also supports `describe()`, lifecycle hooks, mocks, snapshots, and coverage.

It aims for Jest compatibility, but it is not Jest. A large Jest suite may use APIs or environment behavior Bun does not support yet. Try the migration on your real suite before changing CI.

## Bundling JavaScript and TypeScript

Running a server file does not normally require a bundle. Bun can execute the source directly.

Bundling is useful when you want to combine browser modules, reduce the number of files, or prepare code for another runtime.

Bundle an entry point for the browser:

```bash
bun build ./src/app.ts --outdir ./dist
```

The browser is the default target. For Bun server code, set the target explicitly:

```bash
bun build ./server.ts --outdir ./dist --target bun
```

For Node.js, use:

```bash
bun build ./server.ts --outdir ./dist --target node
```

Add minification for a production browser bundle:

```bash
bun build ./src/app.ts --outdir ./dist --minify
```

Bun's bundler understands JavaScript, TypeScript, JSX, and TSX. Like the runtime, it transpiles TypeScript but does not type-check it.

## Creating a standalone executable

Bun can bundle your program together with the Bun runtime.

Create `cli.ts`:

```ts
const name = process.argv[2] ?? 'friend'

console.log(`Hello ${name}`)
```

Run it normally first:

```bash
bun cli.ts Flavio
```

Now compile it:

```bash
bun build --compile ./cli.ts --outfile hello
```

Run the generated executable:

```bash
./hello Flavio
```

The other person does not need Bun or Node.js installed. The executable contains the runtime and your program.

The tradeoff is file size. You are shipping a runtime, not turning JavaScript into a tiny native program.

## Using the built-in SQLite driver

Bun includes a native SQLite driver in `bun:sqlite`.

This makes SQLite a great choice for a small Bun application. You do not need an npm package or a native addon.

Create `database.ts`:

```ts
import { Database } from 'bun:sqlite'

const db = new Database('notes.sqlite')

db.run(`
  CREATE TABLE IF NOT EXISTS notes (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    text TEXT NOT NULL
  )
`)

const insert = db.query('INSERT INTO notes (text) VALUES (?)')
insert.run('Learn Bun')

const notes = db.query('SELECT * FROM notes').all()
console.log(notes)

db.close()
```

Run it:

```bash
bun database.ts
```

The first run creates `notes.sqlite`, creates the table, inserts a note, and reads it back.

Use bound parameters, as we did with `?`, instead of building SQL strings from user input. Bound parameters protect the query from SQL injection.

Bun also has a unified SQL client for PostgreSQL, MySQL, and SQLite. I would still start with the dedicated SQLite API for a small local database because its purpose is very clear.

## How compatible is Bun with Node.js?

Bun wants to run Node.js applications and npm packages with little or no change.

It supports many important Node APIs, including `node:fs`, `node:path`, `Buffer`, `process`, streams, and much of Node-API for native addons. It also understands `package.json` and installs a normal `node_modules` folder.

But Bun is not Node.js.

The engines are different. Some Node APIs are incomplete. Timing, edge cases, native packages, and tools that inspect Node internals can behave differently.

This distinction matters when someone says Bun is a "drop-in replacement." It can feel like one for many projects. It is not a promise that every Node program will work unchanged.

Check Bun's [Node.js compatibility page](https://bun.com/docs/runtime/nodejs-compat) when you depend on a specific API.

## Moving an existing Node.js project to Bun

You do not need to migrate everything at once.

The safest first step is to use Bun only as the package manager:

```bash
bun install
```

Your application can still run with Node. Bun creates a Node-compatible `node_modules` folder.

Review the new `bun.lock`, run the existing tests, and check any dependency that uses an install script.

Next, try the package scripts:

```bash
bun run dev
bun run build
bun test
```

Then run the application itself with Bun:

```bash
bun run src/index.ts
```

Test the parts that touch files, networking, child processes, databases, and native modules. Those boundaries are more likely to reveal runtime differences than ordinary JavaScript code.

When everything works locally, use `bun ci` in CI and test the production build in the same operating system you deploy to.

My advice is to keep the migration boring. Change one layer, verify it, and then change the next one.

## When should you use Bun?

Bun is a good fit when:

- you want to start a TypeScript server with very little setup
- fast installs and fast script startup matter to you
- you like having a runtime, tests, bundling, and package management together
- your dependencies use standard Web APIs or well-supported Node APIs
- you are building a small API, command-line tool, or local utility
- you want to pair Bun with a framework such as Hono

Node.js is still the safer default when you depend on unusual native modules, Node internals, or a platform that only supports Node.

You can also mix the two. Using `bun install` and `bun run` does not force you to use Bun in production.

## The commands you'll use most

Here is the short list I keep in mind:

```bash
bun init                 # create a project
bun index.ts             # run a file
bun --watch index.ts     # restart when files change
bun install              # install dependencies
bun add hono             # add a dependency
bun add -d typescript    # add a development dependency
bun remove hono          # remove a dependency
bun update               # update dependencies
bun run dev              # run a package.json script
bunx tsc --noEmit        # run a package executable
bun test                 # run tests
bun build ./app.ts       # create a bundle
bun upgrade              # update Bun
```

You do not need to memorize all of them. Start with `bun init`, `bun install`, and `bun run`.

## What's next

Bun gives us a fast runtime and a lot of built-in tools. We can run TypeScript, install packages, watch files, test code, serve HTTP, bundle an app, and use SQLite without assembling a toolchain first.

That is the real appeal of Bun to me. The speed is nice. The reduced setup is even better.

Next in this series we'll look at Hono, a small HTTP framework that pairs beautifully with Bun.
