Moving a Node.js Project to Bun: Runtime Internals, Lambda and Migration
Understand what Bun replaces in a Node.js toolchain, why it starts fast, how it runs TypeScript and on AWS Lambda, and how to migrate a project step by step.
This article is published in English.
Sources: Everything You Need to Know About Bun: A Beginner’s Guide, Migration, and Architecture
A typical Node.js service depends on four or five separate tools before it runs a single line of business logic: a package manager, a TypeScript runner, a test framework and a bundler. Bun collapses that stack into one executable that acts as runtime, package manager, bundler and test runner, and it aims to be a drop-in replacement for Node.js. This guide explains what Bun actually replaces, where its speed comes from, how it handles TypeScript and serverless deployment, and how to move an existing project over without surprises. For a broader benchmark-driven comparison that also covers Deno, see Node.js, Deno and Bun compared.
What Bun replaces in a typical toolchain
Running JavaScript outside the browser requires a runtime, and for more than a decade that has meant Node.js. A modern Node project, however, usually assembles several independent tools around it:
- npm or Yarn for installing and managing packages;
tsc,ts-nodeortsxfor compiling or running TypeScript;- Jest or Vitest for unit tests;
- esbuild, webpack or Rollup for production bundles.
Bun ships all of these capabilities in a single binary. Installing it gives you a runtime, a package manager, a bundler, a test runner and direct TypeScript execution without additional configuration. The real benefit is removing the glue: fewer config files and fewer version mismatches between tools.
Weighing the trade-offs against Node.js
The decision mostly comes down to speed and integration on one side and ecosystem maturity on the other.
Where Bun is strong
- One integrated toolchain. There is no separate test runner, bundler or TypeScript transpiler to configure.
- Fast installs. Bun's own measurements put
bun installat up to 30 times faster thannpm; your results will depend on the project and cache state. - Quick cold starts. Processes start in milliseconds, which matters most in serverless environments where cold-start latency is billed and felt by users.
- TypeScript without setup.
.tsand.tsxfiles run directly, with no build step or loader scripts. - Lower memory use. Reported figures for HTTP servers are commonly in the range of 25% to 40% less RAM than Node.js; measure your own workload before relying on that.
- Batteries included.
.envfiles load automatically withoutdotenv, and the runtime includes built-in clients for SQLite, Postgres, S3 and Redis.
Where Node.js still wins
- Maturity. Node.js has more than 15 years of production history and formal Long Term Support releases that enterprises plan around. Bun is younger and still closing compatibility gaps in edge cases.
- Native addons. The large majority of npm packages (often cited as over 95%) work without changes, but older packages that compile C++ code through
node-gypcan still run into problems.
Why Bun starts and runs quickly
It is not simply "Zig beats C++"
Bun is written in Zig, and it is tempting to attribute its speed to the language. In practice, compiled Zig and compiled C++ run at roughly the same speed on the CPU. The difference comes from design. Node.js carries many years of accumulated abstractions, while Bun reimplemented file I/O, string handling and networking from scratch with a strong focus on avoiding unnecessary memory allocations.
A different JavaScript engine
Node.js runs on V8, the engine Google built for Chrome. Bun uses JavaScriptCore, the engine Apple built for Safari. JavaScriptCore is tuned for fast startup and aggressive memory optimisation, which helps explain Bun's cold-start numbers. Each engine has workloads where it is faster, so long-running, compute-heavy services should be benchmarked rather than assumed to improve.
The gains are not limited to macOS
Although JavaScriptCore came out of Safari, both it and Bun are compiled natively for Linux on x86_64 and ARM64 and for Windows. The performance characteristics carry over to ordinary cloud servers regardless of operating system.
Running Bun on AWS Lambda
Bun can serve HTTP backends through its native Bun.serve() API or through familiar frameworks such as Express, Fastify and Hono. On AWS Lambda its fast cold starts can reduce both latency and execution cost. Lambda does not provide a managed Bun runtime to select in the console, so teams typically choose one of three approaches:
- Custom runtime layer. Publish the
bunbinary as a Lambda layer and run it on the base Linux imageprovided.al2023. - Container image. Build the function from the official
oven/bunDocker image, push it to Amazon ECR and deploy it as a container. - Build with Bun, run on Node. Use
bun buildto bundle the code into a single.jsfile, then execute that file on Lambda's managed Node.js runtime.
The third option is the most conservative: you gain Bun's fast bundling while keeping AWS's supported runtime in production. You give up Bun's runtime-specific APIs and cold-start advantage, so it suits teams that want to adopt Bun incrementally.
How Bun executes TypeScript
You run files directly with the bun command, whether they are JavaScript or TypeScript: bun index.js, bun index.ts, or bun --watch index.ts to restart automatically when files change.
When you run a .ts file, Bun's transpiler removes the type annotations on the fly and does so entirely in memory. It does not write a dist/ folder, a .build/ directory or temporary .js files to disk.
The important caveat is that stripping types is not the same as checking them. To keep startup instant, Bun performs no static type verification at runtime, so code with type errors will still run. Type checking stays in your editor, such as VS Code or Cursor, and in CI, where you can run bunx tsc --noEmit. Treat that CI step as mandatory.
Migrating an existing Node.js project step by step
Because Bun follows Node's APIs and npm conventions, a straightforward project usually migrates with a handful of changes. Do it on a branch and run your full test suite at the end.
Step 1: Remove old modules and lockfiles
Start from a clean dependency tree so Bun resolves everything itself: remove the node_modules folder along with whichever lockfile the project used, whether that is package-lock.json, yarn.lock or pnpm-lock.yaml.
Deleting the old lockfile means versions are re-resolved from the ranges in package.json, so you may pick up newer releases than you had before. If exact reproducibility matters, check whether your Bun version can migrate an existing lockfile instead, and review the diff of resolved versions.
Step 2: Install with Bun
Run bun install. Bun reads your existing package.json, downloads the packages and writes a bun.lock file, which you should commit.
Step 3: Update the scripts in package.json
Replace references to node, ts-node or tsx in the scripts section:
- start:
"bun src/index.js"takes over from"node src/index.js"; - dev:
"bun --watch src/index.ts"takes over from"ts-node-dev src/index.ts"; - test:
"bun test"takes over from"jest"or"vitest".
The test runner is largely compatible with Jest's API, but tests that rely heavily on Jest-specific mocking or configuration may need adjustments.
Step 4: Switch the TypeScript definitions
For TypeScript projects, replace the Node type definitions with Bun's by running bun remove @types/node followed by bun add -d @types/bun.
Then update the types entry in the compilerOptions of tsconfig.json. Older guides reference "bun-types" there; with the current @types/bun package the entry is typically "bun", so check the Bun TypeScript docs for the version you install.
Step 5: Replace modules that do not fit
Standard Node modules such as node:fs, node:path and node:http work unchanged. Packages that depend on native C++ bindings are the usual source of trouble, and several have simple substitutes:
- replace
bcryptwithbcryptjsor@node-rs/bcrypt; - replace
sqlite3with Bun's built-inbun:sqlite; - remove
dotenvcalls, since Bun loads.envautomatically.
Be deliberate about that last change. If the same code must still run under Node, for example in the build-only Lambda setup above, keep dotenv or load configuration explicitly so behaviour does not differ between runtimes.
Key takeaways
- Bun's main value is consolidation: one binary replaces the package manager, TypeScript runner, test framework and bundler.
- Its speed comes from allocation-conscious reimplementation and JavaScriptCore's fast startup, not from Zig being inherently faster than C++.
- Bun strips TypeScript types in memory but never checks them, so keep
tsc --noEmitin CI. - On Lambda you choose between a custom layer, a container image, or bundling with Bun and running on managed Node.js.
- Migration is mostly mechanical, but watch lockfile re-resolution, native addons and any behaviour that depends on automatic
.envloading.