Master the art of building blazing-fast web applications using Rust and its powerful ecosystem. Learn how to leverage Rust's safety and performance features for web development. ๐ฆ
Project Setup ๐ฏ
# Create new project
cargo new rust-web-app
cd rust-web-app
# Add dependencies to Cargo.toml
[package]
name = "rust-web-app"
version = "0.1.0"
edition = "2021"
[dependencies]
actix-web = "4.4"
tokio = { version = "1.35", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres"] }
dotenv = "0.15"
jsonwebtoken = "9.2"
bcrypt = "0.15"
validator = { version = "0.16", features = ["derive"] }
tracing = "0.1"
tracing-subscriber = "0.3"
Application Structure ๐
// src/main.rs
use actix_web::{web, App, HttpServer};
use dotenv::dotenv;
use sqlx::PgPool;
use tracing::info;
mod config;
mod handlers;
mod models;
mod errors;
mod middleware;
mod services;
#[actix_web::main]
async fn main() -> std::io::Result<()> {
dotenv().ok();
// Initialize logging
tracing_subscriber::fmt::init();
// Database connection
let database_url = std::env::var("DATABASE_URL")
.expect("DATABASE_URL must be set");
let pool = PgPool::connect(&database_url)
.await
.expect("Failed to connect to Postgres");
// Run migrations
sqlx::migrate!()
.run(&pool)
.await
.expect("Failed to migrate the database");
info!("Starting server at http://127.0.0.1:8080");
HttpServer::new(move || {
App::new()
.app_data(web::Data::new(pool.clone()))
.wrap(middleware::auth::Auth)
.wrap(middleware::logger::Logger)
.configure(config::app_config)
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
Models and Schema ๐
// src/models/user.rs
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
use validator::Validate;
#[derive(Debug, Serialize, Deserialize, FromRow)]
pub struct User {
pub id: i32,
pub email: String,
pub name: String,
#[serde(skip_serializing)]
pub password_hash: String,
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Deserialize, Validate)]
pub struct CreateUser {
#[validate(email)]
pub email: String,
#[validate(length(min = 3))]
pub name: String,
#[validate(length(min = 8))]
pub password: String,
}
#[derive(Debug, Deserialize)]
pub struct LoginUser {
pub email: String,
pub password: String,
}
// src/models/post.rs
#[derive(Debug, Serialize, Deserialize, FromRow)]
pub struct Post {
pub id: i32,
pub title: String,
pub content: String,
pub author_id: i32,
pub published: bool,
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Deserialize, Validate)]
pub struct CreatePost {
#[validate(length(min = 1))]
pub title: String,
#[validate(length(min = 1))]
pub content: String,
pub published: Option<bool>,
}
Database Queries ๐๏ธ
// src/services/user.rs
use sqlx::PgPool;
use crate::models::user::{User, CreateUser};
use crate::errors::AppError;
pub async fn create_user(
pool: &PgPool,
user: CreateUser
) -> Result<User, AppError> {
let password_hash = bcrypt::hash(
user.password.as_bytes(),
bcrypt::DEFAULT_COST
)?;
let user = sqlx::query_as!(
User,
r#"
INSERT INTO users (email, name, password_hash)
VALUES ($1, $2, $3)
RETURNING *
"#,
user.email,
user.name,
password_hash
)
.fetch_one(pool)
.await?;
Ok(user)
}
pub async fn get_user_by_email(
pool: &PgPool,
email: &str
) -> Result<User, AppError> {
let user = sqlx::query_as!(
User,
"SELECT * FROM users WHERE email = $1",
email
)
.fetch_optional(pool)
.await?
.ok_or(AppError::NotFound("User not found".into()))?;
Ok(user)
}
// src/services/post.rs
pub async fn create_post(
pool: &PgPool,
user_id: i32,
post: CreatePost
) -> Result<Post, AppError> {
let post = sqlx::query_as!(
Post,
r#"
INSERT INTO posts (
title,
content,
author_id,
published
)
VALUES ($1, $2, $3, $4)
RETURNING *
"#,
post.title,
post.content,
user_id,
post.published.unwrap_or(false)
)
.fetch_one(pool)
.await?;
Ok(post)
}
pub async fn get_posts(
pool: &PgPool,
limit: i64,
offset: i64
) -> Result<Vec<Post>, AppError> {
let posts = sqlx::query_as!(
Post,
r#"
SELECT * FROM posts
WHERE published = true
ORDER BY created_at DESC
LIMIT $1 OFFSET $2
"#,
limit,
offset
)
.fetch_all(pool)
.await?;
Ok(posts)
}
Authentication Middleware ๐
// src/middleware/auth.rs
use actix_web::{
dev::ServiceRequest,
error::ErrorUnauthorized,
Error,
};
use futures::future::{ok, Ready};
use jsonwebtoken::{decode, DecodingKey, Validation};
pub struct Auth;
impl<S> Transform<S, ServiceRequest> for Auth
where
S: Service<
ServiceRequest,
Response = ServiceResponse,
Error = Error,
>,
{
type Response = ServiceResponse;
type Error = Error;
type Transform = AuthMiddleware<S>;
type InitError = ();
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ok(AuthMiddleware { service })
}
}
pub struct AuthMiddleware<S> {
service: S,
}
impl<S> Service<ServiceRequest> for AuthMiddleware<S>
where
S: Service<
ServiceRequest,
Response = ServiceResponse,
Error = Error,
>,
{
type Response = ServiceResponse;
type Error = Error;
type Future = Pin<Box<dyn Future<
Output = Result<Self::Response, Self::Error>
>>>;
fn poll_ready(
&self,
cx: &mut Context<'_>
) -> Poll<Result<(), Self::Error>> {
self.service.poll_ready(cx)
}
fn call(&self, req: ServiceRequest) -> Self::Future {
if req.path() == "/api/auth/login"
|| req.path() == "/api/auth/register"
{
return Box::pin(self.service.call(req));
}
let auth_header = req
.headers()
.get("Authorization")
.and_then(|h| h.to_str().ok())
.and_then(|h| h.strip_prefix("Bearer "));
if let Some(token) = auth_header {
let secret = std::env::var("JWT_SECRET")
.expect("JWT_SECRET must be set");
match decode::<Claims>(
token,
&DecodingKey::from_secret(secret.as_bytes()),
&Validation::default()
) {
Ok(token_data) => {
req.extensions_mut()
.insert(token_data.claims);
Box::pin(self.service.call(req))
}
Err(_) => Box::pin(
async move {
Err(ErrorUnauthorized("Invalid token"))
}
)
}
} else {
Box::pin(
async move {
Err(ErrorUnauthorized("Missing authorization"))
}
)
}
}
}
Request Handlers ๐ฏ
// src/handlers/auth.rs
use actix_web::{web, HttpResponse};
use crate::models::user::{CreateUser, LoginUser};
use crate::services::user;
use crate::errors::AppError;
pub async fn register(
pool: web::Data<PgPool>,
user: web::Json<CreateUser>
) -> Result<HttpResponse, AppError> {
let user = user.into_inner();
user.validate()?;
let user = user::create_user(&pool, user).await?;
let token = create_token(user.id)?;
Ok(HttpResponse::Ok().json(json!({
"token": token,
"user": user
})))
}
pub async fn login(
pool: web::Data<PgPool>,
credentials: web::Json<LoginUser>
) -> Result<HttpResponse, AppError> {
let user = user::get_user_by_email(
&pool,
&credentials.email
).await?;
let valid = bcrypt::verify(
&credentials.password,
&user.password_hash
)?;
if !valid {
return Err(AppError::Unauthorized(
"Invalid credentials".into()
));
}
let token = create_token(user.id)?;
Ok(HttpResponse::Ok().json(json!({
"token": token,
"user": user
})))
}
// src/handlers/posts.rs
pub async fn create_post(
pool: web::Data<PgPool>,
claims: Claims,
post: web::Json<CreatePost>
) -> Result<HttpResponse, AppError> {
let post = post.into_inner();
post.validate()?;
let post = post::create_post(
&pool,
claims.user_id,
post
).await?;
Ok(HttpResponse::Ok().json(post))
}
pub async fn get_posts(
pool: web::Data<PgPool>,
query: web::Query<Pagination>
) -> Result<HttpResponse, AppError> {
let limit = query.limit.unwrap_or(10);
let offset = query.offset.unwrap_or(0);
let posts = post::get_posts(&pool, limit, offset)
.await?;
Ok(HttpResponse::Ok().json(posts))
}
Error Handling โ ๏ธ
// src/errors.rs
use actix_web::{
error::ResponseError,
http::StatusCode,
HttpResponse,
};
use serde::Serialize;
use std::fmt;
#[derive(Debug)]
pub enum AppError {
NotFound(String),
DatabaseError(sqlx::Error),
ValidationError(validator::ValidationErrors),
Unauthorized(String),
InternalServerError(String),
}
#[derive(Serialize)]
struct ErrorResponse {
code: u16,
message: String,
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
AppError::NotFound(msg) => write!(f, "{}", msg),
AppError::DatabaseError(err) => {
write!(f, "Database error: {}", err)
}
AppError::ValidationError(err) => {
write!(f, "Validation error: {}", err)
}
AppError::Unauthorized(msg) => write!(f, "{}", msg),
AppError::InternalServerError(msg) => {
write!(f, "{}", msg)
}
}
}
}
impl ResponseError for AppError {
fn error_response(&self) -> HttpResponse {
let status = self.status_code();
let message = self.to_string();
HttpResponse::build(status).json(ErrorResponse {
code: status.as_u16(),
message,
})
}
fn status_code(&self) -> StatusCode {
match *self {
AppError::NotFound(_) => StatusCode::NOT_FOUND,
AppError::DatabaseError(_) => {
StatusCode::INTERNAL_SERVER_ERROR
}
AppError::ValidationError(_) => {
StatusCode::BAD_REQUEST
}
AppError::Unauthorized(_) => {
StatusCode::UNAUTHORIZED
}
AppError::InternalServerError(_) => {
StatusCode::INTERNAL_SERVER_ERROR
}
}
}
}
Testing ๐งช
// tests/api/auth.rs
use crate::helpers::spawn_app;
#[tokio::test]
async fn test_register_user() {
let app = spawn_app().await;
let client = reqwest::Client::new();
let response = client
.post(&format!("{}/api/auth/register", &app.address))
.json(&json!({
"email": "test@example.com",
"name": "Test User",
"password": "password123"
}))
.send()
.await
.expect("Failed to execute request");
assert_eq!(200, response.status().as_u16());
let body: serde_json::Value = response
.json()
.await
.expect("Failed to parse response");
assert!(body.get("token").is_some());
assert_eq!(
"test@example.com",
body["user"]["email"].as_str().unwrap()
);
}
#[tokio::test]
async fn test_login_user() {
let app = spawn_app().await;
let client = reqwest::Client::new();
// First register a user
let _ = client
.post(&format!("{}/api/auth/register", &app.address))
.json(&json!({
"email": "test@example.com",
"name": "Test User",
"password": "password123"
}))
.send()
.await
.expect("Failed to execute request");
// Then try to login
let response = client
.post(&format!("{}/api/auth/login", &app.address))
.json(&json!({
"email": "test@example.com",
"password": "password123"
}))
.send()
.await
.expect("Failed to execute request");
assert_eq!(200, response.status().as_u16());
let body: serde_json::Value = response
.json()
.await
.expect("Failed to parse response");
assert!(body.get("token").is_some());
}
Performance Optimization โก
// src/config.rs
use actix_web::web;
use tokio::sync::Mutex;
use std::sync::Arc;
use lru::LruCache;
pub struct AppState {
pub db: PgPool,
pub cache: Arc<Mutex<LruCache<String, Vec<u8>>>>
}
pub fn app_config(cfg: &mut web::ServiceConfig) {
let cache = Arc::new(Mutex::new(
LruCache::new(100)
));
let state = web::Data::new(AppState {
db: pool.clone(),
cache: cache.clone()
});
cfg.app_data(state)
.service(
web::scope("/api")
.configure(auth_config)
.configure(posts_config)
);
}
// src/handlers/posts.rs
pub async fn get_post(
state: web::Data<AppState>,
id: web::Path<i32>
) -> Result<HttpResponse, AppError> {
let cache_key = format!("post:{}", id);
// Check cache first
if let Some(cached) = state
.cache
.lock()
.await
.get(&cache_key)
{
let post: Post = serde_json::from_slice(cached)?;
return Ok(HttpResponse::Ok().json(post));
}
// If not in cache, fetch from database
let post = sqlx::query_as!(
Post,
"SELECT * FROM posts WHERE id = $1",
id.into_inner()
)
.fetch_optional(&state.db)
.await?
.ok_or(AppError::NotFound(
"Post not found".into()
))?;
// Store in cache
let post_json = serde_json::to_vec(&post)?;
state
.cache
.lock()
.await
.put(cache_key, post_json);
Ok(HttpResponse::Ok().json(post))
}
Best Practices ๐
- Use proper error handling
- Implement caching strategies
- Write comprehensive tests
- Use connection pooling
- Implement proper logging
- Use proper validation
- Implement proper security
- Use async/await properly
- Follow Rust best practices
- Document your code
Additional Resources
Building web applications with Rust provides excellent performance and safety guarantees. This guide covers the essential aspects of creating production-ready web applications using Rust and its ecosystem.