Note: Bun recently released HMR features, but as far as I'm aware, this is not Vite nor Vue compatible/integration ready. The solution I've written about here works well enough for me, and I have no intention of changing for development.

Lately I've been on a big Typescript kick, slowly forcing myself to do proper typing in my code and really enjoying it. I haven't actively written code in a while and it's been nice to come back. I'm not formally educated in programming in any way, so excuse me here if anything I say is wrong.

Server and client monorepo complications

When initially approaching my development using Bun, I was fairly lost. I tried to use the Bun.serve() to redirect certain endpoints to a baked index.html with Bun's simple HTML import feature, which while it worked it did not allow debugging with Vue's wonderful developer tools effectively.

While a single process is really nice for production, it is less so when you can't debug your client code. Instead, I realized one of the two processes needs to proxy to the other. My initial approach on this particular project was to look at Bun's APIs and find a proxy.

While I found some solutions for reverse proxying connections with modules from npm, that seemed incorrect and primed for digging myself into a hole. That's when I remembered Vite has a fully functional proxy option built in, which works perfectly for this task.

The Vite Solution

While I don't have a ton of experience in development professionally, My perception is that this is the type of knowledge that people do not know as they rely on copy paste jobs, AI, or other tools which prevent you from RTFM.

If we look at the Vite DefineConfig object in the docs, we find there's a server key and proxy child key that allow defining a proxy. My configuration now looks like so:

import { fileURLToPath, URL } from "node:url";

import { defineConfig, loadEnv } from "vite";
import vue from "@vitejs/plugin-vue";
import vueDevTools from "vite-plugin-vue-devtools";

// https://vite.dev/config/
export default defineConfig(({ mode }) => {
  const env = loadEnv(mode, process.cwd(), "");
  const host = `http://${import.meta.env.DOMAIN || "localhost"}:${
    import.meta.env.BACKEND_PORT || "3001"
  }`;
  console.log(`vite config using backend ${host}`);
  return {
    plugins: [vue(), vueDevTools()],
    resolve: {
      alias: {
        "@": fileURLToPath(new URL("./src", import.meta.url)),
      },
    },
    server: {
      port: Number(env.FRONTEND_PORT || 3000),
      strictPort: true,
      proxy: {
        "/api": host,
      },
    },
  };
});

You can see the relative simplicity in this approach, enabling me to proxy the entire /api endpoint to my backend Bun process. This ensures that the WebSocket and any other necessary traffic can go to Vite without issue, but all API calls go to the backend.

import.meta.env is a bit of magic from Vite, the documentation is very useful!

Perfect is the enemy of good

While I think this approach is probably needing some improvement, it's allowed me to focus on the actual work at hand and not write a bunch of extra code on the backend side to handle frontend calls. In the future I'd to rewrite this, but for now this works wonders for basic frontend hobbyists such as myself.

If you have any extensive thoughts and opinions, or suggestions for better workflows, yell at me on Bluesky!