$ cd ..

Running Remix with Bun

📅 2024-10-31

⌛ 14 days ago


Before anything, go ahead and install Bun:

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

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.

Using Bun for dependencies

As simple as running:

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:

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.

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

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:

{
  "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. 🚀