Migrating a TypeScript app from Node.js to Bun
Tags: typescript, node, webdev
Published: 2023-03-17 13:29:50

**Written by [John Reilly](https://blog.logrocket.com/author/johnreilly/)✏️**

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](https://typestrong.org/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](https://bun.sh/). 

_Jump ahead:_

*   [Setting up the ts-node app](#setting-up-ts-node-app)
*   [Installing Bun](#installing-bun)
*   [Porting the install from Yarn to Bun](#porting-install-yarn-bun)
*   [Switching from `@types/node` to `bun/types`](#switching-types-node-bun-types)
*   [Addressing `moduleResolution` with Bun](#addressing-moduleresolution-bun)
*   [Filing APIs with Bun](#filing-apis-bun)
    *   [Clarification about the `fs.promises` API](#clarification-fs-promises)
*   [Running the app](#running-app)
*   [Top-level `await` with Bun](#top-level-await-bun)
*   [GitHub Actions and Bun](#github-actions-bun)
*   [Performance comparison: Bun vs. ts-node](#performance-comparison-bun-ts-node)

## Setting up the ts-node app <a name="setting-up-ts-node-app">

I have a [technical blog](https://johnnyreilly.com/) which is built on Docusaurus. When the Docusaurus build completes, a post processing script runs to do things like:

*   update the `sitemap.xml` to include the `lastmod` date, based on [git commit date](https://johnnyreilly.com/docusaurus-createfeeditems-api-git-commit-date), and truncate the number of entries in the file
*   patch the HTML files to use [Cloudinary](https://cloudinary.com) as an image CDN for Open Graph images

These 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](https://github.com/johnnyreilly/blog.johnnyreilly.com/tree/main/trim-xml%5D). 

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.

## Installing Bun <a name="installing-bun">

I installed Bun on my Ubuntu machine using the following command:

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

The resulting output looked like this:

```bash
>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](https://github.com/oven-sh/bun/pull/2175). When I ran the suggested commands it looked like Bun was happy and healthy.

## Porting the install from Yarn to Bun <a name="porting-install-yarn-bun">

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`:

```bash
cd trim-xml
bun install
```

This resulted in the following output:

```bash
bun install v0.5.7 (5929daee)
 + @types/node@18.14.1
 + fast-xml-parser@4.1.2
 + simple-git@3.16.1
 + typescript@4.9.5

 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 read`bun.lockb` files](https://github.com/JacksonKearl/bun-lockb) 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?

## Switching from `@types/node` to `bun/types` <a name="switching-types-node-bun-types">

As 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`](https://oven-sh.github.io/bun-types/) package. I added it to my project, while removing `@types/node`and `ts-node`:

```typescript
bun remove @types/node
bun remove ts-node
bun add bun-types
```

Here’s how the output looked:

```typescript
 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 bun-types@0.5.7

 1 packages installed [1.97s]
```

The [docs also say](https://oven-sh.github.io/bun-types/#usage) to add the following code to your `tsconfig.json` or `jsconfig.json` file:

```json
{
  "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:

```json
  {
    "compilerOptions": {
-      "target": "ES2022",
+      "target": "esnext",
-      // "lib": [],
+      "lib": ["ESNext"],
-      "module": "NodeNext",
+      "module": "esnext",
-      // "types": [],
+      "types": ["bun-types"],
    },
  }
```

## Addressing `moduleResolution` with Bun <a name="addressing-moduleresolution-bun">

At 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: ![VS Code Cannot Find Module Fast-XML-Parser](https://blog.logrocket.com/wp-content/uploads/2023/03/vs-code-cannot-find-module-fast-xml-parser.png)

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:

```json
  {
    "compilerOptions": {
-      // "moduleResolution": "node",
+      "moduleResolution": "nodenext",
    },
  }
```

With this change in place, the module resolution errors were... resolved. (Sorry!)

## Filing APIs with Bun <a name="filing-apis-bun">

Even though the module resolution errors were resolved, I was still getting other errors. This time they were about the [`fs.promises` API](https://nodejs.org/api/fs.html#promises-api): ![VS Code Reporting Absence FS-Promises API](https://blog.logrocket.com/wp-content/uploads/2023/03/vs-code-reporting-absence-fs-promises-api.png)

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.readdir`
*   `await fs.promises.readFile`
*   `await fs.promises.writeFile`

I was able to replace the `fs.promises.readFile` and `fs.promises.writeFile` with the Bun equivalents [`Bun.file(path).text()`](https://bun.sh/docs/api/file-io#reading-files)and [`Bun.write(path, content)`](https://bun.sh/docs/api/file-io#writing-files), respectively:

```typescript
- `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](https://nodejs.org/api/fs.html#fsreaddirsyncpath-options):

```typescript
- `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.

### Clarification about the `fs.promises` API <a name="clarification-fs-promises">

As 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](https://twitter.com/jarredsumner/status/1629818921904902145) that the `fs.promises` API is implemented but the types aren't as of this writing: https://twitter.com/jarredsumner/status/1629818921904902145

## Running the app <a name="running-app">

Before running the app, I needed to do one more thing:

```typescript
-    "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:

```bash
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.

## Top-level `await` and Bun <a name="top-level-await-bun">

The 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:

```typescript
- main();
+ await main();
```

I began getting the expected log messages; and the program appeared to be working as expected.

## GitHub Actions and Bun <a name="github-actions-bun">

I was now able to run the app locally. But I wanted to run it in [GitHub Actions](https://github.com/features/actions). I just needed to add the `setup-bun` action to my workflow, so Bun would be available in the GitHub Actions environment:

```bash
- name: Setup bun 🔧
  uses: oven-sh/setup-bun@v1
  with:
    bun-version: latest
```

## Performance comparison: Bun vs. ts-node <a name="performance-comparison-bun-ts-node">

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:

```bash
Post processing finished in 17.09 seconds
Done in 19.52s.
```

Running the app in GitHub Actions with Bun:

```bash
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:

1.  "Post processing" reflects the time taken to run the `main` function
2.  "Done" reflects the time taken to run the `bun` command end to end

What 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.

## Conclusion

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.

---

## 200’s only ✔️ Monitor failed and slow network requests in production

Deploying 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] (https://logrocket.com/signup/). 

[![LogRocket Network Request Monitoring](https://blog.logrocket.com/wp-content/uploads/2019/12/network-request-filter-2-1.png)](https://lp.logrocket.com/blg/signup) 

[LogRocket] (https://lp.logrocket.com/blg/signup) is like a DVR for web apps, recording literally everything that happens on your site. Instead of guessing why problems happen, you can aggregate and report on problematic network requests to quickly understand the root cause.

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] (https://lp.logrocket.com/blg/signup).