axum-vite
axum-vite is a utility crate that simplifies the integration between an Axum backend and a Vite frontend.
It enables a "Single Binary" developer experience: during development, your Axum server acts as a reverse proxy to the Vite dev server, providing seamless Hot Module Replacement (HMR). In production, it serves pre-built assets embedded directly into the Rust binary.
Why axum-vite?
In many Rust + Frontend workflows, developers face a choice between two frictions:
- The Two-Server Problem: Running the backend and frontend separately, dealing with CORS, and managing two different ports in the browser.
- The Restart Problem: Using
cargo-watchto rebuild the binary on every change, which destroys the fast, partial hot-reload experience that Vite provides.
axum-vite solves this by making the Axum server the single entry point. You get the convenience of one URL (localhost:3000) and the speed of Vite's HMR, without the overhead of manual proxy configuration.
Features
- Transparent Proxying: Automatically forwards requests to the Vite dev server in debug mode.
- Header Preservation: Forwards crucial headers (like
Accept) so Vite can correctly serve CSS as stylesheets instead of JS modules. - Embedded Assets: Uses
include_dirto serve built assets from the binary in release mode. - Auto-Spawn: Optionally starts the Vite dev server as a child process on startup.
- Env-Driven Config: Configure ports, roots, and commands via environment variables.
Quick Start
A runnable example lives in examples/. It pairs a minimal Axum server with a Vite + React frontend (generated by following https://vite.dev/guide) — no configuration needed to try it out:
# Terminal 1 — start the frontend
&&
# Terminal 2 — start the backend (proxies frontend → Vite)
Then open http://localhost:3000.
Embedding into your own project
Add the dependency
[]
= "0.1.0"
Wire up the router
use Router;
use ;
async
$CARGO_MANIFEST_DIR is always set by Cargo at compile time, so no extra environment variables are required for release builds. In debug mode the include_dir! call inside the macro is not compiled at all — you can cargo run without building the frontend first.
Configuration
You can configure the crate via ViteConfig or environment variables:
| Field | Env Var | Default | Description |
|---|---|---|---|
dev_port |
VITE_PORT |
5173 |
Port of the Vite dev server. |
prefix |
VITE_STATIC_PREFIX |
/static/ |
URL prefix for assets. |
frontend_root |
VITE_ROOT |
None |
Absolute path to the Vite project root. |
dev_command |
VITE_DEV_CMD |
npm run dev |
Command used to start Vite. |
auto_start |
VITE_AUTO_START |
false |
Whether to spawn Vite on startup. |
framework |
VITE_FRAMEWORK |
none |
Frontend framework for HMR preamble (react, vue, svelte). |
dev_host |
VITE_DEV_HOST |
localhost |
Hostname of the Vite dev server. |
All fields are public, so you can construct ViteConfig directly instead of using from_env — useful when you want compile-time config or don't want environment variables involved:
use PathBuf;
use ;
let config = ViteConfig ;
Logging
axum-vite uses the log facade. Hook it up with any compatible logger (e.g. env_logger).
| Level | When |
|---|---|
info |
Dev server spawned on startup. |
trace |
Every proxied request in dev mode — silent by default, useful for debugging. |
warn |
Dev server unreachable; 404s in release mode. |
debug |
Successful static file served in release mode. |
# See every proxied request during development
RUST_LOG=axum_vite=trace
# See only warnings and above (recommended for normal dev)
RUST_LOG=axum_vite=warn
Workflow
- Development: Run your Rust server. It proxies frontend to Vite. You get instant HMR.
- Build: Run
npm run buildto generate thedistfolder. - Production: Pass
axum_vite::embedded_dir!("$CARGO_MANIFEST_DIR/dist")toViteConfig::from_env. The macro embeds the folder into your binary at compile time and the crate automatically switches from proxying to serving those embedded files.
Serving HTML
There are two approaches depending on who generates your HTML:
Option A — Vite owns index.html (most projects)
Use spa_router. In dev mode it proxies / directly to the Vite dev server, so Vite injects the HMR preamble itself. No VITE_FRAMEWORK needed.
use Router;
use ;
let config = from_env;
let app = new
.route
.merge;
API routes registered before spa_router take priority over the SPA catch-all.
Option B — your template engine owns index.html (Askama, Tera, MiniJinja, …)
Because Axum renders the HTML, Vite never gets a chance to inject its preamble. You must call config.hmr_scripts() in your template and set VITE_FRAMEWORK so the crate knows which preamble to generate.
If HMR is not working, this is the most likely reason: you are rendering your own HTML but haven’t called
hmr_scripts()in the template.
// In your Axum handler:
let config = from_env;
let hmr = config.hmr_scripts; // empty string in release builds
// Pass `hmr` into your template context.
<!-- In your base template: -->
{{ title }}
{{ hmr_scripts|safe }}
Set VITE_FRAMEWORK=react (or vue / svelte) so the correct preamble is generated. You do not need spa_router or hmr_injection_middleware in this setup.
Known Limitations
HMR Preamble only applies to template-based setups
VITE_FRAMEWORK and hmr_scripts() are only needed when you render HTML
server-side (e.g. Askama) and call hmr_scripts() in your template. When
using spa_router, index.html is proxied directly from the Vite dev server
which already injects the React Refresh preamble itself — VITE_FRAMEWORK has
no effect and can be omitted.
The framework-specific preamble code is hardcoded in this crate and may lag behind framework plugin releases. If HMR stops working after a plugin upgrade (browser console shows "can't detect preamble"), check the plugin's source for the expected preamble and open an issue or PR.