In web development and deployment, most software engineers are familiar with either:
- Separating the built SPA and the backend (Client-Side Rendering), or
- Return HTML directly from the backend (Server-Side Rendering)
I recently (re)discovered 1 that there is a third way: embedding the built SPA into the backend’s binary file, and serving it directly.

I think this is an elegant approach, as the pros are:
- Simpler deployment as we only have one binary file in the end
- Simpler code where we don’t have to take into account CORS and the backend endpoint since the frontend and backend are served from the same origin 2
The cons are quite clear:
- No matter how good is it, it’s still an unconventional approach; expect lots of push back from people
- Increased binary size and memory usage because of the static file embedding
- Slightly reduced DX due to no frontend hot reloading 2
In more details, the steps are:
- Build the frontend
- Copy the frontend built artifact to backend’s static folder serving
- Run the backend binary
- (for production environment) Try to embed the built frontend to the backend’s binary before deploying
Let’s get into how are we doing it with Rust/Axum and Svelte/SvelteKit 3. While the code that I’m going to show you is in that specific stack, I think it’s not challenging to adopt the mindset to other languages and frameworks and libraries 4.
Project Structure
For simplicity, I’d start with a monorepo setup, where the backend and the frontend are in separate folders in the same Git repository. I’m sure the same end result is achievable with a polyrepo setup, given enough effort.
. ├── ... ├── packages │ ├── frontend │ └── backend └── README.mdI’m using monorepo tool called Moon [^moonrepo]. By the developers’ terms, it sits:
- Above task runners like Make or Just
- Below full-blown tools like Bazel
It has both build cache and task dependency built-in, adding “just enough” structure to the tasks around a monorepo.
Frontend
Here is the frontend folder structure:
. ├── ... ├── moon.yml ├── package.json ├── README.md ├── src │ ├── ... │ ├── lib │ │ └── index.ts │ └── routes │ ├── child-url │ │ └── +page.svelte │ ├── +layout.svelte │ ├── +page.svelte │ └── ... └── ...The home page at /, which has its source at src/routes/+page.svelte would try to demonstrate a very simple data fetching from the backend. There is a link to a child URL to see if navigation works:
Moving into the frontend folder, let’s assume that we have a command to build the project, and the result is an SPA in build/:
Backend
Before we start the backend, we have to move the built frontend folder to the backend to be served. We could do it manually with cp, but I found a more elegant solution : symlink-ing. We don’t have move the files “physically” as they are accessible through the symlink:
Now, the structure within the backend folder:
. ├── Cargo.lock ├── Cargo.toml ├── frontend-build -> ../frontend/build ├── moon.yml └── src ├── frontend.rs └── main.rsThere isn’t a lot to care about, except frontend.rs, where the “magic sauce” is placed:
The library we use is rust-embed, and in fact, I reused most of the library’s example code 5. The code in main.rs using Axum is quite straightforward:
For production deployment, rust-embed would automatically include static files in the binary.
Demonstration
We can start the backend and the frontend building process:
Here is a GIF to show you how would the end result look:

Conclusion
After this relatively short post, I hope I showed you how would serving an SPA in Rust backend work. Again, you can try tweaking the code 3 yourself.
.png)


