Iāve wanted to take a look at some of the alternative JavaScript runtimes for a while. The thing that has held me back is npm compatibility. I want to be able to run my code in a runtime that isnāt Node.js and still be able to use npm packages. Iāve been using ts-node for a long time now; itās what I reach for when Iām building any kind of console app.
In this post Iāll investigate just how easy it is to port a TypeScript app from Node.js to Bun.
Jump ahead:
@types/node to bun/typesmoduleResolution with Bunawait with BunThe Replay is a weekly newsletter for dev and engineering leaders.
Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.
I have a technical blog which is built on Docusaurus. When the Docusaurus build completes, a post processing script runs to do things like:
sitemap.xml to include the lastmod date, based on git commit date, and truncate the number of entries in the fileThese scripts are implemented as a simple ts-node console app. For historical reasons itās called trim-xml (it originally just truncated the sitemap.xml file). Itās not a particularly good name but Iām not going to change it now. As the blog is open source, you can see the code of trim-xml here.
What weāre interested in, is porting this app from ts-node to Bun. The app has a few dependencies, so npm compatibility is important to us. Letās see how it goes.
I installed Bun on my Ubuntu machine using the following command:
curl -fsSL https://bun.sh/install | bash
The resulting output looked like this:
>bun was installed successfully to ~/.bun/bin/bun Added "~/.bun/bin" to $PATH in "~/.zshrc" To get started, run: exec /usr/bin/zsh bun --help
I was a little weirded out by the inconsistent indentation in the output, but Iām sure thatās just a formatting issue. I submitted a PR to fix it. When I ran the suggested commands it looked like Bun was happy and healthy.
With Bun in place, I was ready to port the app. I opened up the (as I say, badly named) trim-xml directory and triggered installation of the dependencies using bun install:
cd trim-xml bun install
This resulted in the following output:
bun install v0.5.7 (5929daee) + @types/[email protected] + [email protected] + [email protected] + [email protected] 5 packages installed [2.34s]
In addition a new bun.lockb file appeared in the directory alongside the package.json file.
Although I canāt find any supporting documentation, Iām guessing this is the Bun equivalent of a package-lock.json or yarn.lock file. Itās a binary file, so it canāt be read. However, I did find this project that allows you to readbun.lockb files and looks like a useful way to solve that problem.
To avoid confusion, I also deleted the yarn.lock file. Yay ā Iāve installed things! And, pretty fast! Whatās next?
@types/node to bun/typesAs I looked at the output for the install, I realized that the @types/node package had been installed. The @types/node package contains TypeScript definitions for the Node.js runtime. Given that I was planning to use Bun, it seemed unlikely that Iād need these. But, I probably would need something that represented the Bun runtime types (iIncidentally, I imagine these would be pretty similar to the Node.js runtime types).
I had a quick look at Bunās documentation and found the bun/types package. I added it to my project, while removing @types/nodeand ts-node:
bun remove @types/node bun remove ts-node bun add bun-types
Hereās how the output looked:
bun remove v0.5.7 (5929daee) - @types/node 1 packages removed [3.00ms] bun remove v0.5.7 (5929daee) - ts-node 1 packages removed [843.00ms] bun add v0.5.7 (5929daee) installed [email protected] 1 packages installed [1.97s]
The docs also say to add the following code to your tsconfig.json or jsconfig.json file:
{
"compilerOptions": {
"lib": ["ESNext"],
"module": "esnext",
"target": "esnext",
// "bun-types" is the important part
"types": ["bun-types"]
}
}
I aligned my existing tsconfig.json file with the above. For my console app, this meant the following changes:
{
"compilerOptions": {
- "target": "ES2022",
+ "target": "esnext",
- // "lib": [],
+ "lib": ["ESNext"],
- "module": "NodeNext",
+ "module": "esnext",
- // "types": [],
+ "types": ["bun-types"],
},
}
moduleResolution with BunAt this point, I thought Iād be able to run the app. However, when I navigated around in VS Code I saw that I had a bunch of errors:

The error message suggested that I needed to explicitly state that I wanted to use the Node.js module resolution algorithm. Weāre using Bun, but weāre porting a Node app, so this made sense.
To address this issue I made one more change to the tsconfig.json file:
{
"compilerOptions": {
- // "moduleResolution": "node",
+ "moduleResolution": "nodenext",
},
}
With this change in place, the module resolution errors were⦠resolved. (Sorry!)
Even though the module resolution errors were resolved, I was still getting other errors. This time they were about the fs.promises API:

It looked like the version of Bun I was using didnāt support that API. As I dug through my code I realized that I was using the fs.promises API in a few places. I was using it in the following ways:
await fs.promises.readdirawait fs.promises.readFileawait fs.promises.writeFileI was able to replace the fs.promises.readFile and fs.promises.writeFile with the Bun equivalents Bun.file(path).text()and Bun.write(path, content), respectively:
- `await fs.promises.readFile` + `await Bun.file(path).text()` - `await fs.promises.writeFile(path, content)` + `await Bun.write(path, content)`
But, there did not appear to be a Bun equivalent for fs.promises.readdir, so I used the sync Node.js API:
- `await fs.promises.readdir` + `fs.readdirSync(path)`
Finally, the code was error-free (at least in VS Code, as far as TypeScript was concerned). But, I had yet to run the app to see if it actually worked.
fs.promises APIAs I was working through addressing the fs.promises API error issue, I tweeted about my findings. Jarred Sumner (who works on Bun) was kind enough to share that the fs.promises API is implemented but the types arenāt as of this writing:
Jarred Sumner on Twitter: āit sort of exists, but looks like the types are out of date. I say sort of because, actually everything async is sync for node:fs and it just wraps in a Promise If you use fs createReadStream / fs.createWriteStream or Bun.file(path).stream() itāll be concurrent / async / Twitterā
it sort of exists, but looks like the types are out of date. I say sort of because, actually everything async is sync for node:fs and it just wraps in a Promise If you use fs createReadStream / fs.createWriteStream or Bun.file(path).stream() itāll be concurrent / async
Before running the app, I needed to do one more thing:
- "start": "ts-node index.ts" + "start": "bun index.ts"
Thatās right, update the start script in package.json to use bun instead of ts-node.
Now I was able to run the app using the bun start command:
Loading /home/john/code/github/blog.johnnyreilly.com/blog-website/build/sitemap.xml Reducing 526 urls to 512 urls
The first positive thing I saw was that I appeared to have running code. Yay!
The program also appeared to be executing instantaneously, which seemed surprising. I was expecting Bun to be fast, but this seemed too fast! Also, it lacked many of the log messages Iād expect. I was expecting to see about 1000 log messages. Something wasnāt right.
await and BunThe issue was that my main function was asynchronous. However, because support for top-level await wasnāt available in Node.js when I originally wrote the code, Iād called the main function synchronously. Fortunately Node didnāt complain about that, and the program behaved as required.
However, Bun looked like it was respecting the fact that main was asynchronous. Thatās why it was apparently executing so quickly; it wasnāt waiting for the main method to complete before terminating.
To be honest, Bunās behavior here is just right; the code didnāt suggest that it was interested in waiting for the main function to complete. But, it turns out that waiting is exactly the desired behavior. To put things right, I could use top-level await.
So, I made the following change to the index.ts file:
- main(); + await main();
I began getting the expected log messages; and the program appeared to be working as expected.
I was now able to run the app locally. But I wanted to run it in GitHub Actions. I just needed to add the setup-bun action to my workflow, so Bun would be available in the GitHub Actions environment:
- name: Setup bun š§
uses: oven-sh/setup-bun@v1
with:
bun-version: latest
I was expecting Bun to be faster than ts-node. Letās take a run of the app in GitHub Actions with ts-node and compare it to a run of the app with Bun:
Running the app in GitHub Actions with ts-node:
Post processing finished in 17.09 seconds Done in 19.52s.
Running the app in GitHub Actions with Bun:
Post processing finished in 12.367 seconds Done in 12.72s.
I havenāt done any formal benchmarking, but it looks like Bun is about 50% faster than ts-node for this use case. Thatās pretty good.
Itās also worth expanding on how this breaks down. Youāll notice in the logs above there are two log entries:
main functionbun command end to endWhat can we learn from this? First of all, running code in ts-node takes 17sec, compared to 12sec with Bun. So, Bun is performing about 40% faster at running code.
Running the command end to end takes 19sec with ts-node, compared to 14sec with Bun. So Bun is performing about 50% faster end to end. There are two parts to this: the time taken to compile the code and the time taken to start up. Also, weāre type checking with ts-node ā if it were deactivated, that would make a difference.
However, when you look at the difference between the end-to-end runtime and code runtime with Bun, itās a mere 0.353sec. ts-node clocks in at 2.43sec for the same. So, ts-node is about 6.5 times slower at starting up. Thatās a pretty big difference; itās unlikely that all of this is TypeScript compilation; Node.js is fundamentally slower at getting going compared to Bun.
Moving from ts-node to Bun was a pretty easy process. I was able to do it in a few hours. I was able to run the app locally and also in GitHub Actions. Also, I was able to run the app in less time.
This all makes me feel very positive about Bun. Iām looking forward to using it more in the future.
Monitor failed and slow network requests in productionDeploying a Node-based web app or website is the easy part. Making sure your Node instance continues to serve resources to your app is where things get tougher. If youāre interested in ensuring requests to the backend or third-party services are successful, try LogRocket.
LogRocket lets you replay user sessions, eliminating guesswork around why bugs happen by showing exactly what users experienced. It captures console logs, errors, network requests, and pixel-perfect DOM recordings ā compatible with all frameworks.
LogRocket's Galileo AI watches sessions for you, instantly identifying and explaining user struggles with automated monitoring of your entire product experience.
LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. Start monitoring for free.

Vibe coding isnāt just AI-assisted chaos. Hereās how to avoid insecure, unreadable code and turn your āvibesā into real developer productivity.

GitHub SpecKit brings structure to AI-assisted coding with a spec-driven workflow. Learn how to build a consistent, React-based project guided by clear specs and plans.

:has(), with examplesThe CSS :has() pseudo-class is a powerful new feature that lets you style parents, siblings, and more ā writing cleaner, more dynamic CSS with less JavaScript.

Kombai AI converts Figma designs into clean, responsive frontend code. It helps developers build production-ready UIs faster while keeping design accuracy and code quality intact.
Would you be interested in joining LogRocket's developer community?
Join LogRocketās Content Advisory Board. Youāll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up now