Full-Stack Rust Project: Blogging Website using Actix Web 3 and Tera
1. Overview and Core Premise
Building a full-stack web application in Rust allows developers to harness maximum performance, memory safety, and robust type checking without relying on complex, memory-heavy Node.js or Python runtimes. This project implements a complete CRUD (Create, Read, Update, Delete) blogging platform entirely on the backend using Actix Web 3 for HTTP routing and server management, SeaORM for asynchronous database operations with MySQL, and Tera (a Handlebars-inspired template engine) for server-side HTML rendering.
By handling processing, pagination, and template rendering on the backend, this project avoids the overhead of managing a separate frontend framework (like React or Vue), keeping the architecture clean, cohesive, and exceptionally fast.
2. Real-Life Analogy: The Backend Chef vs. The Full-Stack Line Cook
Analogy: Building a server-side rendered application with Actix Web and Tera is like running a small boutique diner where the chef stands right behind the counter.
In a traditional Single Page Application (SPA) with a decoupled frontend (React) and backend (Node/Express), the backend is like a wholesale warehouse that only ships raw JSON ingredients across town, leaving a separate crew in the front to assemble the plates.
In our Actix Web + Tera architecture, the backend server acts as a master chef who plates the entire dish in the kitchen. When a browser requests a page, Actix fetches the raw data from the MySQL database pantry via SeaORM, hands those ingredients to Tera (our plating station) to bake them directly into a finished HTML document, and serves the hot, fully assembled page straight to the client's browser in a single trip.
3. Technology Stack & Architecture
The application is structured into modular components: an entity model package for database communication, a migration package for schema setup, static asset folders for styling, Tera templates for UI views, and the core Actix Web routing engine.
4. Visualizing the Application Flow
The data flow between the browser client, Actix Web server, and MySQL database follows a strict vertical pipeline for every user interaction:
5. Project Directory & Package Structure
To maintain clean separation of concerns and enable independent module compilation, the project is split into distinct workspace members:
6. Environment Configuration (.env)
The application requires environment variables for network binding and database connectivity.
HOST=127.0.0.1
PORT=8000
DATABASE_URL=mysql://root:password@127.0.0.1:3306/actix_example
Note: When running MySQL inside Docker, ensure you use the official
mysql:latestimage rather than specialized server variants to avoid manual permission and configuration mismatches. Create the database (actix_example) inside your container before launching the application.
7. Database Entity Modeling (entity/src/post.rs)
SeaORM bridges Rust structs and MySQL relational tables. The Post entity defines columns for id, title, and text.
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
#[sea_orm(table_name = "posts")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub title: String,
#[sea_orm(column_type = "Text")]
pub text: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}
8. Actix Web Server Initialization (src/main.rs)
The main function is annotated with #[actix_web::main] to run an asynchronous event loop. It loads environment variables, initializes database connections, runs schema migrations, configures Tera templates, and binds the HTTP server.
use actix_web::{App, HttpServer, web, middleware::Logger};
use tera::Tera;
use sea_orm::Database;
#[actix_web::main]
async fn main() -> std::io::Result<()> {
std::env::set_var("RUST_LOG", "debug");
env_logger::init();
dotenv::dotenv().ok();
let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL is not set");
let host = std::env::var("HOST").unwrap_or_else(|_| "127.0.0.1".into());
let port = std::env::var("PORT").unwrap_or_else(|_| "8000".into());
let server_url = format!("{}:{}", host, port);
// Connect to MySQL Database via SeaORM
let db = Database::connect(&database_url).await.expect("Failed to connect to database");
// Initialize Tera templates from templates directory
let templates = Tera::new("templates/**/*.html").expect("Failed to parse templates");
// App State shared across routes
#[derive(Clone)]
struct AppState {
templates: Tera,
con: sea_orm::DatabaseConnection,
}
let state = AppState {
templates,
con: db,
};
println!("Starting server at http://{}", server_url);
HttpServer::new(move || {
App::new()
.data(state.clone())
.wrap(Logger::default())
// Register services and static file handler
.configure(init_services)
.service(actix_files::Files::new("/static", "./static").show_files_listing())
})
.bind(server_url)?
.run()
.await
}
9. Route Mapping and Service Configuration
The application defines six core service endpoints handling CRUD operations and page rendering:
Here is how these services are registered in the configuration function:
fn init_services(cfg: &mut web::ServiceConfig) {
cfg.service(
web::resource("/")
.route(web::get().to(list))
.route(web::post().to(create))
)
.service(web::resource("/new").route(web::get().to(new)))
.service(
web::resource("/{id}")
.route(web::get().to(edit))
.route(web::post().to(update))
)
.service(web::resource("/delete/{id}").route(web::post().to(delete)));
}
10. Implementing Core CRUD Handlers
A. Creating a New Post (create)
The create handler parses form data (title and text), wraps it into a SeaORM ActiveModel, saves it to the database, flashes a success message, and redirects the user to the homepage.
use actix_web::{HttpResponse, Result, web};
use serde::Deserialize;
#[derive(Deserialize)]
pub struct PostForm {
pub title: String,
pub text: String,
}
pub async fn create(
data: web::Data<AppState>,
form: web::Form<PostForm>,
) -> Result<HttpResponse> {
let db = &data.con;
let form = form.into_inner();
let post = entity::post::ActiveModel {
title: sea_orm::Set(form.title),
text: sea_orm::Set(form.text),
..Default::default()
};
post.insert(db).await.expect("Could not insert post");
Ok(HttpResponse::Found()
.header("LOCATION", "/")
.finish())
}
B. Rendering the Creation Form (new)
pub async fn new(data: web::Data<AppState>) -> Result<HttpResponse> {
let templates = &data.templates;
let ctx = tera::Context::new();
match templates.render("new.html", &ctx) {
Ok(body) => Ok(HttpResponse::Ok().content_type("text/html").body(body)),
Err(err) => Ok(HttpResponse::InternalServerError().body(err.to_string())),
}
}
C. Fetching and Editing by ID (edit)
pub async fn edit(
data: web::Data<AppState>,
id: web::Path<i32>,
) -> Result<HttpResponse> {
let db = &data.con;
let templates = &data.templates;
let post_id = id.into_inner();
let post = entity::post::Entity::find_by_id(post_id)
.one(db)
.await
.expect("Could not find post")
.expect("Post not found");
let mut ctx = tera::Context::new();
ctx.insert("post", &post);
match templates.render("edit.html", &ctx) {
Ok(body) => Ok(HttpResponse::Ok().content_type("text/html").body(body)),
Err(err) => Ok(HttpResponse::InternalServerError().body(err.to_string())),
}
}
D. Updating a Post (update)
pub async fn update(
data: web::Data<AppState>,
id: web::Path<i32>,
form: web::Form<PostForm>,
) -> Result<HttpResponse> {
let db = &data.con;
let post_id = id.into_inner();
let form = form.into_inner();
let post: entity::post::ActiveModel = entity::post::ActiveModel {
id: sea_orm::Set(post_id),
title: sea_orm::Set(form.title),
text: sea_orm::Set(form.text),
};
post.update(db).await.expect("Could not update post");
Ok(HttpResponse::Found()
.header("LOCATION", "/")
.finish())
}
E. Deleting a Post (delete)
pub async fn delete(
data: web::Data<AppState>,
id: web::Path<i32>,
) -> Result<HttpResponse> {
let db = &data.con;
let post_id = id.into_inner();
let post: entity::post::ActiveModel = entity::post::Entity::find_by_id(post_id)
.one(db)
.await
.expect("Could not find post")
.expect("Post not found")
.into();
post.delete(db).await.expect("Could not delete post");
Ok(HttpResponse::Found()
.header("LOCATION", "/")
.finish())
}
11. Industry Reality & Debugging Mindset
When building and running Actix Web applications with MySQL and Docker in production or local development, developers frequently encounter specific failure modes:
- WSL Package Conflicts: Running complex Rust toolchains with native C-bindings (like MySQL connectors) inside Windows Subsystem for Linux (WSL) can occasionally cause linker or package version mismatches. Utilizing an isolated Virtual Box environment guarantees a clean package tree and reproducible builds.
- Database Connection Lag: When starting the Actix server concurrently with Docker containers, the MySQL server may take a few seconds to accept TCP connections. If the server panics with a connection refused error on startup, ensure the Docker container is fully healthy before executing
cargo run. - Template Path Resolution: Tera relies on relative paths to locate template files (
templates/**/*.html). If templates fail to load at runtime, verify your working directory matches the root of the Cargo project where thetemplates/folder resides.
12. Summary and Key Takeaways
- High-Performance Backend Routing: Actix Web 3 provides robust, lightning-fast asynchronous routing and middleware support widely adopted in production Rust environments.
- Server-Side Rendering with Tera: Using Tera eliminates the need for heavyweight frontend frameworks (React/Vue) by processing templates directly on the server, mirroring Handlebars syntax.
- Modular Cargo Workspaces: Separating database entities (
entity/) and schema migrations (migration/) into distinct sub-packages keeps codebases organized and compilation modular. - SeaORM Integration: SeaORM bridges asynchronous Rust code and relational MySQL databases cleanly using
ActiveModelfor inserts, updates, and deletes. - Complete CRUD Lifecycle: Form parsing (
web::Form), path parameter extraction (web::Path), and redirection (HttpResponse::Found) complete the full web application cycle. - Environment Management: Decoupling configuration via
.envfiles anddotenvensures secure credential and port management across local and production deployments.