Bun 1.4 — install to a running HTTP server

1 month ago

That response came back from a server running on Bun 1.4. Its entire package.json is the six lines on the right, and no dependency list appears anywhere in it. There is no node_modules folder beside it either. Getting here from a machine that has never had Bun on it is one install line and one file of TypeScript, and one reason does the work three times over.

Ask

Ask about this presentation

Answers are generated from this presentation.

Chapters

  1. 0:00A server answering with zero packages installed
  2. 0:23One line installs Bun
  3. 0:44One build of Bun for every command
  4. 1:11bun init writes five files
  5. 1:27The only two entries are type definitions
  6. 1:51bun compiles the TypeScript itself
  7. 2:18A route is a value you hand to Bun.serve
  8. 3:04Image, markdown and cron are built in
  9. 3:42One Bun.serve call plus an error handler
  10. 4:04Every route answers, including the one that throws
  11. 4:29Five routes, 71 packages on Node and zero on Bun
  12. 5:01A private registry in .npmrc answers 401
  13. 5:27A compiled binary macOS 27 will not launch
  14. 5:54The symlinked layout caused the Vite crash
  15. 6:39Twenty two lines with zero import statements
  16. 6:58The four issues in this episode are public
Show transcript

A server answering with zero packages installed

This HTTP server answers requests with zero packages installed
Bun 1.4.0 · 34cbb9a40
$ curl -s localhost:3000/health OK
$ cat package.json { "name": "manual-bun14", "module": "index.ts", "type": "module", "private": true } $ ls node_modules ls: node_modules: No such file or directory
Source: captured 2026-08-27 on Bun 1.4.0+34cbb9a40 · assets/terminal/06, 08a, 08b

That response came back from a server running on Bun 1.4. Its entire package.json is the six lines on the right, and no dependency list appears anywhere in it. There is no node_modules folder beside it either. Getting here from a machine that has never had Bun on it is one install line and one file of TypeScript, and one reason does the work three times over.

One line installs Bun

01
Install
One line installs Bun, and Linux needs unzip first
Linux · sudo apt install unzip
Homebrew · brew upgrade bun
$ curl -fsSL https://bun.com/install | bash ######################################################################## 100.0% bun was installed successfully to ~/.bun-versions/1.4.0/bin/bun Run 'bun --help' to get started
Source: bun.com/docs/installation · assets/data/doc-installation.txt · assets/terminal/01-install.txt

Install is one line. Curl the install script from bun dot com and pipe it into bash. Two things the page says that people skip. On Linux the unzip package has to be there first, or the installer stops. And a Homebrew install upgrades with brew upgrade bun, because bun upgrade there puts two package managers on one binary.

One build of Bun for every command

Every command in this episode ran on one build of Bun
1.4.0 · 34cbb9a40
released 2026-08-20 · the first Bun written in Rust
$ bun --version 1.4.0 $ bun --revision 1.4.0+34cbb9a40
Source: assets/terminal/02-version-revision.txt · bun.com/blog/bun-v1.4

Then two commands, and people skip the second one. Bun dash dash version prints 1.4.0. Bun dash dash revision prints 1.4.0 plus 34cbb9a40, the commit that binary was built from. A version number on its own cannot reproduce a bug, so the revision string is what an issue report needs. Bun 1.4.0 landed on August the twentieth, and the release post calls it the first Bun written in Rust.

bun init writes five files

02
First run
bun init writes five files and no build config
$ bun init ? Select a project template - Press return to submit. ❯ Blank React Library + .gitignore + CLAUDE.md + index.ts + tsconfig.json (for editor autocomplete) + README.md
Source: bun init on Bun 1.4.0+34cbb9a40, 2026-08-27 · assets/terminal/03-bun-init-blank.txt, 03b-bun-init-template-prompt.ansi

Run bun init. It asks which template you want: Blank, React or Library, and Blank is the default. It writes five files. A gitignore, a CLAUDE dot md, an index dot ts, a tsconfig dot json and a README. That is the entire project.

The only two entries are type definitions

The only two entries bun init wrote are type definitions for your editor
$ bun run index.ts Hello via Bun!
this file has no dependencies key
$ cat package.json { "name": "manual-bun14", "module": "index.ts", "type": "module", "private": true, "devDependencies": { "@types/bun": "latest" }, "peerDependencies": { "typescript": "^7" } }
type definitions · editor autocomplete only
Source: assets/terminal/04-run-hello.txt, 07-package-json-after-init.txt

Bun run index dot ts prints Hello via Bun. There was no build step, no tsc call, and no TypeScript runner in package.json. That is the first dependency simply gone. Now look at what bun init did leave. Two entries, and both are type definitions. Types slash bun, and typescript as a peer dependency. They feed your editor's autocomplete, and no code imports them. The file has no dependencies key at all.

bun compiles the TypeScript itself

03
The shape
bun is the only thing on this machine that compiles the TypeScript
$ bun run index.ts Hello via Bun!
Source: assets/terminal/04-run-hello.txt, run 2026-08-27 on Bun 1.4.0+34cbb9a40 · screenshot: bun.com/docs/runtime/http/server

The build step is missing for a reason. The program that turns TypeScript into something runnable lives inside the bun executable that just ran your file. In the Node world that program is a separate install, and you keep its version in step with your TypeScript version by hand. Here there is nothing separate to keep in step. One dependency deleted, and the mechanism is one sentence: the tool moved inside the runtime. The next two moves are that same sentence with different tools in it.

A route is a value you hand to Bun.serve

A route is a value you hand to Bun.serve
the keys are path patterns · the values are what to answer with
port → $BUN_PORT, $PORT, $NODE_PORT, 3000
hostname → 0.0.0.0
idleTimeout → 10s
Source: bun.com/docs/runtime/http/server, captured 2026-08-27

If you have used Express, you register each route by calling a function once per path. Bun dot serve takes a plain object instead. Slash health maps to a bare Response holding the string OK, a response object sitting in a data structure. Slash users slash colon i-d maps to a function, and the colon segment arrives as req dot params dot i-d. One key can hold a GET and a POST side by side. There is no app dot use and no router package, because a router is a lookup table and the server already owns one. Second dependency deleted, same mechanism. The routes option needs Bun 1.2.3 or newer, so routing is not what 1.4 added. What 1.4 changed is everything around it.

Image, markdown and cron are built in

Image resizing, markdown and cron are now built in
Bun.Imageresizes an upload
Bun.markdownrenders markdown
Bun.cronruns a nightly job
Bun.Image resize blends the stored numbers, not the light they stand for
#40510 · open · both backends
Source: bun.com/blog/bun-v1.4 · each name checked present on 1.4.0 and absent on 1.2.23 · github.com/oven-sh/bun/issues/40510

The utilities a small service reaches for next are in the binary too. Bun dot Image resizes an upload. Bun dot markdown renders markdown. Bun dot cron runs a nightly job. Those three built-ins are new in 1.4, and here is where they stop. When Bun dot Image shrinks an image it blends the stored numbers rather than the light those numbers stand for, so the result comes out a shade off. That is issue 40510, open as of this recording. For this milestone the runtime is enough. For image work you ship to users, check Bun dot Image against the library you already trust.

One Bun.serve call plus an error handler

04
The real thing
The whole server is one Bun.serve call plus an error handler
const server = Bun.serve({ port: 3000, routes: { "/health": new Response("OK"), "/users/:id": req => Response.json({ id: req.params.id }), "/notes": { GET: () => Response.json({ notes: ["first"] }), POST: async req => { const body = await req.json(); return Response.json({ created: body }, { status: 201 }); }, }, "/boom": () => { throw new Error("kaboom"); }, }, error(error) { return new Response(`error: ${error.message}\n`, { status: 500 }); }, }); console.log(`Listening on ${server.url}`);
Source: assets/data/index.ts, run 2026-08-27 on Bun 1.4.0+34cbb9a40

Now build the server the title promised. Start from the quickstart's Bun dot serve call and grow it. It gets a static route at slash health, a parameter route at slash users slash colon i-d, and a GET and POST pair at slash notes where the POST awaits req dot json. The error handler turns a thrown exception into a real response instead of a stack trace.

Every route answers, including the one that throws

Every route answers, including the one that throws
$ bun run index.ts Listening on http://localhost:3000/
$ curl -s localhost:3000/health OK $ curl -s localhost:3000/users/42 {"id":"42"} $ curl -s localhost:3000/notes {"notes":["first"]} $ curl -s -X POST localhost:3000/notes -H 'content-type: application/json' -d '{"text":"hello"}' {"created":{"text":"hello"}} $ curl -s -i localhost:3000/boom | head -1 HTTP/1.1 500 Internal Server Error $ curl -s localhost:3000/boom error: kaboom
Source: assets/terminal/05-serve-listening.txt, 06-curl-all-routes.txt

Bun run index dot ts prints Listening on localhost port 3000. In the second pane, curl every route. Slash health returns OK. Slash users slash 42 returns the id 42 as JSON. Posting a JSON body to slash notes echoes it back under the key created. And slash boom returns a 500 with the message kaboom, because the error handler caught the exception.

Five routes, 71 packages on Node and zero on Bun

The same five routes need 71 packages on Node and zero on Bun
$ cat package.json { "name": "manual-bun14", "module": "index.ts", "type": "module", "private": true, "devDependencies": { "@types/bun": "latest" }, "peerDependencies": { "typescript": "^7" } }$ cat package.json { "name": "manual-bun14", "module": "index.ts", "type": "module", "private": true } $ ls node_modules ls: node_modules: No such file or directory$ bun run index.ts Listening on http://localhost:3000/ $ curl -s localhost:3000/health OK
Bun 1.4.0Node 25 + express + tsx
Direct dependencies02
Packages on disk071
node_modulesnone15M
the Bun column is 0 after the two type definition entries are removed
Source: measured 2026-08-27 · assets/data/dependency-count.json · assets/terminal/07, 08a, 08b, 09

Now read the package.json. There is no dependencies block in it, only the two type definition entries bun init wrote back in step two. So delete those two entries. Delete node_modules. Run the server again. It still answers. Zero packages on disk, and the server is serving. Beside it, the same five routes built on Node. Express supplies the router and the JSON body parsing, and tsx runs the TypeScript. That is two direct dependencies, 71 packages on disk and 15 megabytes.

A private registry in .npmrc answers 401

05
Gotchas
A private registry in .npmrc answers 401 on Bun 1.4.0
token stored for /api/v4/packages/
→ registry lives at /api/v4/packages/npm/
Bun sends no auth header · registry answers 401
#40549 · open · fix PR #40424 in review
Source: github.com/oven-sh/bun/issues/40549, state checked 2026-08-27

This is the part people get wrong. A private registry that works under npm fails under Bun 1.4.0, and the reason is a string match. Bun matches the auth token key against the registry path exactly, while npm walks that path upward. So a token stored one path segment up never applies, and the registry answers 401. Issue 40549 is open, and its fix is still in review.

A compiled binary macOS 27 will not launch

bun build with the compile flag produces a binary macOS 27 will not launch
macOS 26 and earlier · runs anyway
macOS 27 · killed at launch
fix PR #39837 · merged 2026-08-22 · on main
1.4.0 is still the latest release, so an installable build with this fix does not exist yet
Source: github.com/oven-sh/bun/issues/39764 and PR #39837, state checked 2026-08-27

People get this one wrong on macOS. The compiled output fails Apple's code signature check, and macOS 26 and earlier ran it anyway, so the same broken binary only started dying when people upgraded to macOS 27. Issue 39764 is closed, fixed by pull request 39837, merged on August the twenty second. That fix is on main, not in a build you can install today.

The symlinked layout caused the Vite crash

The isolated linker did not cause the Vite crash; the symlinked layout did
bun --linker=hoistedHTTP 200 ✓
npmHTTP 200 ✓
bun --linker=isolatedHTTP 500 ✗
pnpm, no Bun involvedHTTP 500 ✗
# bunfig.toml [install] linker = "hoisted"
#39963 · closed as not planned
Source: github.com/oven-sh/bun/issues/39963 and its triage control comment · upstream TanStack/router#8139

The third one is a misdiagnosis, and the diagnosis is the part people get wrong. The isolated linker is an install mode you turn on with a flag, and it gives every package one shared copy on disk instead of a copy per project. People blamed that flag when Vite and Nitro server rendering died with a use before initialization error. Bun's triage ran the controls. The hoisted linker and npm both return a 200. The isolated linker and pnpm both return a 500, and the pnpm run has no Bun in it at all. The failure follows the symlinked layout, so the bug went upstream. Any toolchain that breaks under pnpm breaks under that flag too. Set linker to hoisted in bunfig dot toml.

Twenty two lines with zero import statements

The whole server is twenty two lines with zero import statements
const server = Bun.serve({ port: 3000, routes: { "/health": new Response("OK"), "/users/:id": req => Response.json({ id: req.params.id }), "/notes": { GET: () => Response.json({ notes: ["first"] }), POST: async req => { const body = await req.json(); return Response.json({ created: body }, { status: 201 }); }, }, "/boom": () => { throw new Error("kaboom"); }, }, error(error) { return new Response(`error: ${error.message}\n`, { status: 500 }); }, }); console.log(`Listening on ${server.url}`);
$ cat package.json { "name": "manual-bun14", "module": "index.ts", "type": "module", "private": true } $ ls node_modules ls: node_modules: No such file or directory
Source: assets/data/index.ts, run 2026-08-27 on Bun 1.4.0+34cbb9a40

Here is the whole file, and it is the file that was running for every curl in this episode. Twenty two lines, one Bun dot serve call, and zero import statements at the top. Beside it, the package.json, with no dependencies block and no node_modules folder to go with it.

The four issues in this episode are public

The four issues in this episode are public, and every state was checked today
Bun 1.4.0 · 34cbb9a40
#40549 open #39764 closed #39963 closed #40510 open
Source: bun.com/docs · github.com/oven-sh/bun, all states checked 2026-08-27

So take one question back to your own project. How many of the packages sitting in your package.json are doing work the runtime now does by itself?