Rust Web Development with Tailwind CSS, LiveJS and Bacon
Published: 2025-02-25
Intro
I wanted to share a pretty cool workflow I figured out for developing web application with rust. It's been working well for me while I migrate this site from static web hosting on AWS to dynamic local hosting in containers.
Bacon is a tool that watches your code for changes and can run commands such has cargo check and cargo test as you are working automagically on save.
In this post, I will show you how I setup Bacon to rebuild my website when files change, including rebuilding the CSS with Tailwind CLI and using LiveJS when only in dev mode.
The best thing about this workflow: hot reloading of content on project file saves. LET'S GOOOOO!
Software
The following software was used in this post.
- Rust - 1.86.0
- TailwindCSS - v4.0.0
- LiveJS - 4
- Bacon - 3.8.0
Project Structure
The following directory structure is applicable to this project.
.
├── bin/dev
├── dev/
├── src/
├── www/
├── Cargo.toml
├── bacon.toml
└── tw
TailwindCSS
I am using TailwindCSS in my project, and use the standalone binary to compile the application assets. The below commands download the binary and make it executable.
# Download Tailwind CLI
curl -L https://github.com/tailwindlabs/tailwindcss/releases/download/v4.0.0/tailwindcss-linux-x64 -o tw
# make executable
chmod +x tw
LiveJS
LiveJS is a small piece of Javascript code that you add to you site when in development mode. It sends continuous HEAD requests and updates/reloads the page on changes.
I use an environment variable ENVIRONMENT to switch between the dev and prod environments.
When the environment is set to dev, the LiveJS script is added to the site <head></head> tag. The LiveJS script is not added during prod deployements.
Below is how I apply this in the Rust code.
use std::env;
use std::sync::OnceLock;
#[derive(Debug, Default)]
pub enum Environment {
Prod,
#[default]
Dev,
}
impl From<String> for Environment {
fn from(value: String) -> Self {
match value.to_lowercase().as_str() {
"prod" => Environment::Prod,
_ => Environment::Dev,
}
}
}
impl Environment {
pub fn get() -> Environment {
env::var("ENVIRONMENT")
.map(Environment::from)
.unwrap_or_default()
}
}
fn init_env() -> Environment {
Environment::get()
}
pub fn get_env() -> &'static Environment {
ENVIRONMENT.get_or_init(init_env)
}
match get_env() {
Environment::Prod => Comment::new().text("PRODUCTION").build(),
Environment::Dev => {
Script::new()
.attrs(
Attrs::new()
.src(format!("/{ASSETS_DIR}/{JS_DIR}/{DEV_DIR}/{LIVE_JS}"))
.build(),
)
.build()
}
}
Bacon
Bacon is a background code checker, which can be used to watch files and run commands when files change.
Check out the Bacon docs, for installation instructions and detailed usage.
The below config file snippet tells Bacon to watch for changes in the src/ and www/assets/js/ directories, and run the bin/app dev script when there are changes.
[jobs.dev]
command = ["bin/app", "dev"]
need_stdout = true
env = { "RUST_LOG" = "debug" }
background = false
on_change_strategy = "kill_then_restart"
kill = ["pkill", "-TERM", "-P"]
watch = [
"src/", "www/assets/js/"
]
Bash Script
I use a Bash script to manage common tasks for development and deployment.
Bacon can only run a single command at a time. My workaround for this is to wrap the tasks in the Bash script and have Bacon execute that. Once again Bash saves the day.
In the below Bash script, the dev case will run tailwind cli in dev mode to compile the CSS, and run then run the webserver in dev mode via cargo run
#! /bin/sh
CMD=$1;
tailwind_dev() {
./tw -i dev/input.css -o www/assets/css/app/app.css;
}
case $CMD in
"dev")
tailwind_dev;
ENVIRONMENT="dev", WEB_PORT=9001 cargo run;
;;
# .. more commands
esac
Usage
To start watching files, use the bacon dev command. This will run the [jobs.dev] block defined in the bacon.toml file. When files in the watched directories are saved, the CSS will be rebuilt and the web server will be restarted.
And the best part! Thanks to the magic of LiveJS, the webpage will be reloaded with the new content.
Outro
In this post, I showed you how to setup live reloading for your Rust web application development, using TailwindCSS, LiveJS and Bacon. If you got this far, I hope you have found some value in this post.
Stay Weird ✌️