---
title: "Running Remix with Bun"
description: "Changing Remix's default server from Node.js to Bun"
datePublished: 2024-10-31T03:45:00.000Z
slug: "running-remix-with-bun"
previewImage: "/preview/remix-bun.jpg"
permalink: "/blog/running-remix-with-bun"
---
<br />

Before anything, go ahead and install Bun:

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

<div class="border-l-2 border-yellow-400 px-4">
	**Note**: I haven't really even benchmarked the performance of Bun's HTTP server vs.
	`remix-server` (with Node.js) but Bun's dependency management is just so *fast* I couldn't resist.
</div>

## Using Bun for dependencies

As simple as running:

```bash
bun install
```

And in most cases it will migrate over your `package-lock.json` to a `bun.lockb` file.

## Using Bun as the runtime

If you've been using `remix-serve` (like me), you probably don't have a `server.ts` file. Go ahead and create one:

```typescript
import { createRequestHandler, logDevReady } from "@remix-run/server-runtime";
import { broadcastDevReady, type ServerBuild } from "@remix-run/node";
import * as build from "./build/server/index.js";
import { join } from "path";

const remix_build = build as unknown as ServerBuild;
const handler = createRequestHandler(remix_build, process.env.NODE_ENV);
const port = process.env.PORT || 3000;

console.log(`🚀 Server starting on port ${port}`);

if (process.env.NODE_ENV === "development") {
	broadcastDevReady(remix_build);
	logDevReady(remix_build);
}

Bun.serve({
	port: port,
	async fetch(request: Request) {
		try {
			const url = new URL(request.url);

			// Try serving static files from public directory
			let file = Bun.file(join("public", url.pathname));
			if (await file.exists()) {
				const headers = new Headers();
				headers.set("Cache-Control", "public, max-age=31536000, immutable");

				if (url.pathname.endsWith(".js")) {
					headers.set("Content-Type", "application/javascript");
				} else if (url.pathname.endsWith(".css")) {
					headers.set("Content-Type", "text/css");
				} else if (url.pathname.endsWith(".html")) {
					headers.set("Content-Type", "text/html");
				}
				return new Response(file, { headers });
			}

			// Handle Vite's build output assets
			if (url.pathname.startsWith("/assets/")) {
				// Try client build directory
				file = Bun.file(join("build/client", url.pathname));
				if (await file.exists()) {
					const headers = new Headers();
					if (url.pathname.endsWith(".js")) {
						headers.set("Content-Type", "application/javascript");
					} else if (url.pathname.endsWith(".css")) {
						headers.set("Content-Type", "text/css");
					}
					return new Response(file, { headers });
				}
			}

			// Handle Remix routes
			const loadContext = {};
			return handler(request, loadContext);
		} catch (error) {
			console.error("Error processing request:", error);
			return new Response("Internal Server Error", { status: 500 });
		}
	},
	error(error) {
		console.error("Server error:", error);
		return new Response("Server Error", { status: 500 });
	},
});
```

The above is a simple example but you may want to add a few more things nice-to-haves like [`xHomu`'s server here](https://github.com/manawiki/mana/blob/main/core.server.ts).

Finally, we tell Vite to not bundle Node.js built-in modules (since Bun will handle them):

```typescript
import { vitePlugin as remix } from "@remix-run/dev";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
import { builtinModules } from "module";

export default defineConfig({
	optimizeDeps: {
		exclude: [...builtinModules],
	},
	plugins: [
		remix({
			future: {
				v3_fetcherPersist: true,
				v3_relativeSplatPath: true,
				v3_throwAbortReason: true,
			},
		}),
		tsconfigPaths(),
	],
});
```

At least for my use case, I'm leaving `dev` and `build` scripts as is:

```json
{
	"scripts": {
		"build": "remix vite:build",
		"dev": "remix vite:dev",
		"start": "bun run server.ts"
	}
}
```

Fire off `bun run start` and that's it! You're now running Remix with Bun. 🚀