Serve Static Files, Handle APIs, Add Security & Connect to a Database — All in Rust
Fast, reliable server with Actix-web
Serve HTML & JSON files
Complete RESTful operations
API Keys & JWT authentication
Connect with SQLx & Postgres
cargo new rust_api_server
cd rust_api_server
cargo add actix-web && more ...
In this comprehensive tutorial, you'll learn how to build a complete backend API server using Rust. We'll cover everything from project setup to database integration.
Create a fast, secure HTTP server using Actix-web framework
Serve HTML and JSON data efficiently with proper content types
Implement GET, POST, PATCH, DELETE endpoints with proper request handling
Implement API Key validation and JWT authentication for secure access
Connect to PostgreSQL/MySQL using SQLx with async queries and connection pooling
Installing Rust, creating a new project, and adding required dependencies
Building routes, handlers, and implementing complete CRUD operations
Adding authentication layers and connecting to a database with async queries
Get your project environment ready with these simple steps.
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
cargo new rust_api_server
cd rust_api_server
[dependencies]
actix-web = "4.3.1"
actix-files = "0.6.2"
serde = {version = "1.0", features = ["derive"]}
serde_json = "1.0"
jsonwebtoken = "8.3.0"
sqlx = {version = "0.7", features = ["runtime-tokio-rustls", "postgres"]}
tokio = {version = "1", features = ["full"]}
dotenv = "0.15.0"
rust_api_server/
├── Cargo.toml
├── .env
├── src/
│ ├── main.rs
│ ├── routes.rs
│ ├── handlers.rs
│ ├── models.rs
│ ├── db.rs
│ └── middleware/
│ ├── auth.rs
│ └── mod.rs
└── static/
└── index.html
Set up files and directories according to the structure above
Create .env file with database connection string and API keys
Learn how to serve HTML and JSON files through your API
Learn how to serve HTML files and static JSON data from your Rust API server.
Actix-web makes it easy to serve static files like HTML, CSS, JavaScript, and images from your server. This is perfect for single-page applications or simple websites.
use actix_web::{App, HttpServer};
use actix_files as fs;
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.service(fs::Files::new("/static", "./static")
.show_files_listing())
})
.bind("127.0.0.1:8080")?
.run()
.await
}
First, create a "static" directory in your project root to store your static files
Create an index.html file in the static directory with your content
Use Files::new() to map a URL path to your static directory
Files will be accessible at http://localhost:8080/static/index.html
You can also serve static JSON data which is useful for providing configuration or initial data for your applications.
use actix_web::{get, web, App, HttpResponse, HttpServer};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
struct Config {
app_name: String,
version: String,
features: Vec<String>,
}
#[get("/api/config")]
async fn get_config() -> HttpResponse {
let config = Config {
app_name: "RustAPI".to_string(),
version: "1.0.0".to_string(),
features: vec![
"Static Content".to_string(),
"CRUD Operations".to_string(),
"Security".to_string(),
"Database".to_string(),
],
};
HttpResponse::Ok().json(config)
}
Pro Tip:
For larger JSON data, consider loading it from files rather than hardcoding it in your Rust code.
Implement complete RESTful API endpoints with Create, Read, Update, and Delete operations.
CRUD represents the four basic operations that can be performed on any data:
POST requests to add new resources
GET requests to retrieve resources
PUT/PATCH requests to modify resources
DELETE requests to remove resources
use actix_web::{web, App, HttpServer, HttpResponse, Responder};
use actix_web::{get, post, patch, delete};
use serde::{Deserialize, Serialize};
use std::sync::{Arc, Mutex};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Item {
pub id: Option<u64>,
pub name: String,
pub description: String,
}
#[derive(Debug, Deserialize)]
pub struct ItemUpdate {
pub name: Option<String>,
pub description: Option<String>,
}
// Application state with shared items vector
pub struct AppState {
pub items: Mutex<Vec<Item>>,
}
#[get("/api/items")]
pub async fn get_items(data: web::Data<AppState>) -> impl Responder {
let items = data.items.lock().unwrap();
HttpResponse::Ok().json(&*items)
}
#[get("/api/items/{id}")]
pub async fn get_item(
path: web::Path<u64>,
data: web::Data<AppState>
) -> impl Responder {
let id = path.into_inner();
let items = data.items.lock().unwrap();
if let Some(item) = items.iter().find(|i| i.id == Some(id)) {
HttpResponse::Ok().json(item)
} else {
HttpResponse::NotFound().body("Item not found")
}
}
#[post("/api/items")]
pub async fn create_item(
item: web::Json<Item>,
data: web::Data<AppState>
) -> impl Responder {
let mut items = data.items.lock().unwrap();
let mut new_item = item.into_inner();
// Generate new ID
let new_id = items.len() as u64 + 1;
new_item.id = Some(new_id);
items.push(new_item.clone());
HttpResponse::Created().json(new_item)
}
#[patch("/api/items/{id}")]
pub async fn update_item(
path: web::Path<u64>,
item_update: web::Json<ItemUpdate>,
data: web::Data<AppState>
) -> impl Responder {
let id = path.into_inner();
let mut items = data.items.lock().unwrap();
if let Some(item) = items.iter_mut().find(|i| i.id == Some(id)) {
if let Some(name) = &item_update.name {
item.name = name.clone();
}
if let Some(desc) = &item_update.description {
item.description = desc.clone();
}
HttpResponse::Ok().json(item)
} else {
HttpResponse::NotFound().body("Item not found")
}
}
#[delete("/api/items/{id}")]
pub async fn delete_item(
path: web::Path<u64>,
data: web::Data<AppState>
) -> impl Responder {
let id = path.into_inner();
let mut items = data.items.lock().unwrap();
let initial_len = items.len();
items.retain(|i| i.id != Some(id));
if items.len() != initial_len {
HttpResponse::Ok().body("Item deleted")
} else {
HttpResponse::NotFound().body("Item not found")
}
}
// Configure routes function
pub fn config_app(cfg: &mut web::ServiceConfig) {
cfg.service(get_items)
.service(get_item)
.service(create_item)
.service(update_item)
.service(delete_item);
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
// Initialize application state with some sample items
let app_state = web::Data::new(AppState {
items: Mutex::new(vec![
Item {
id: Some(1),
name: "Sample Item".to_string(),
description: "This is a sample item".to_string(),
}
]),
});
HttpServer::new(move || {
App::new()
.app_data(app_state.clone())
.configure(config_app)
})
.bind("127.0.0.1:8080")?
.run()
.await
}
use serde::{Deserialize, Serialize};
use std::sync::{Arc, Mutex};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Item {
pub id: Option<u64>,
pub name: String,
pub description: String,
}
#[derive(Debug, Deserialize)]
pub struct ItemUpdate {
pub name: Option<String>,
pub description: Option<String>,
}
#[get("/api/items")]
pub async fn get_items(data: web::Data<AppState>) -> HttpResponse {
let items = data.items.lock().unwrap();
HttpResponse::Ok().json(&*items)
}
#[get("/api/items/{id}")]
pub async fn get_item(
path: web::Path<u64>,
data: web::Data<AppState>
) -> HttpResponse {
let id = path.into_inner();
let items = data.items.lock().unwrap();
if let Some(item) = items.iter().find(|i| i.id == Some(id)) {
HttpResponse::Ok().json(item)
} else {
HttpResponse::NotFound().body("Item not found")
}
}
#[post("/api/items")]
pub async fn create_item(
item: web::Json<Item>,
data: web::Data<AppState>
) -> HttpResponse {
let mut items = data.items.lock().unwrap();
let mut new_item = item.into_inner();
// Generate new ID
let new_id = items.len() as u64 + 1;
new_item.id = Some(new_id);
items.push(new_item.clone());
HttpResponse::Created().json(new_item)
}
#[patch("/api/items/{id}")]
pub async fn update_item(
path: web::Path<u64>,
item_update: web::Json<ItemUpdate>,
data: web::Data<AppState>
) -> HttpResponse {
let id = path.into_inner();
let mut items = data.items.lock().unwrap();
if let Some(item) = items.iter_mut().find(|i| i.id == Some(id)) {
if let Some(name) = &item_update.name {
item.name = name.clone();
}
if let Some(desc) = &item_update.description {
item.description = desc.clone();
}
HttpResponse::Ok().json(item)
} else {
HttpResponse::NotFound().body("Item not found")
}
}
#[delete("/api/items/{id}")]
pub async fn delete_item(
path: web::Path<u64>,
data: web::Data<AppState>
) -> HttpResponse {
let id = path.into_inner();
let mut items = data.items.lock().unwrap();
let initial_len = items.len();
items.retain(|i| i.id != Some(id));
if items.len() != initial_len {
HttpResponse::Ok().body("Item deleted")
} else {
HttpResponse::NotFound().body("Item not found")
}
}
Protect your API with multiple layers of authentication and authorization.
A simple yet effective way to control access to your API using API keys in request headers.
use actix_web::{dev::ServiceRequest, Error};
use actix_web::error;
use std::env;
pub async fn api_key_middleware(
req: ServiceRequest,
next: actix_web::dev::ServiceResponse,
) -> Result<actix_web::dev::ServiceResponse, Error> {
// Get expected API key from environment
let expected_key = env::var("API_KEY")
.unwrap_or_else(|_| "default_api_key".to_string());
// Check if the request has the correct API key
if req.headers().get("x-api-key")
.map_or(false, |key| key == expected_key.as_str()) {
// API key is valid, continue with the request
Ok(next)
} else {
// API key is missing or invalid
Err(error::ErrorUnauthorized("Invalid API key"))
}
}
Add your middleware to the application in main.rs
App::new().wrap(middleware::ApiKey)
Store your API key securely as an environment variable
API_KEY=your_secret_api_key
Clients must include the API key in request headers
curl -H "x-api-key: your_secret_api_key" http://localhost:8080/api/items
Protect sensitive routes with middleware that validates authentication before allowing access.
Implement role-based authorization to control what actions different users can perform on your API.
Securely store user passwords using Rust's bcrypt or argon2 crates for strong cryptographic hashing.
Protect your API from abuse by implementing rate limiting to restrict the number of requests from a single source.
Implement secure, stateless token-based authentication with JSON Web Tokens.
User submits credentials and receives a token
Client stores the JWT in local storage or cookies
Client sends JWT with each request to protected routes
Organize your Rust API server with a clean, maintainable folder structure.
Each module has a single responsibility and well-defined boundaries
Structure reflects your domain entities and their relationships
Code is organized in modules that can be developed and tested independently
Follow the same patterns and naming conventions throughout the codebase
main.rs
clean and delegate to other modulesCargo.toml
for optional componentsThe recommended organization for a production-ready Rust API server:
rust_api_server/
├── Cargo.toml # Project manifest
├── .env # Environment variables
├── .gitignore
├── src/
│ ├── main.rs # Entry point
│ ├── config.rs # Application configuration
│ ├── server.rs # Server setup and lifecycle
│ ├── routes.rs # Route definitions
│ ├── models/ # Data structures
│ │ ├── mod.rs # Module exports
│ │ ├── user.rs # User model
│ │ └── item.rs # Item model
│ ├── handlers/ # Request handlers
│ │ ├── mod.rs # Module exports
│ │ ├── auth.rs # Authentication handlers
│ │ └── items.rs # Item handlers
│ ├── middleware/ # Custom middleware
│ │ ├── mod.rs # Module exports
│ │ ├── auth.rs # Auth middleware
│ │ └── logging.rs # Logging middleware
│ ├── db/ # Database operations
│ │ ├── mod.rs # Module exports
│ │ ├── connection.rs # Connection pool setup
│ │ └── repositories/ # Database repositories
│ │ ├── mod.rs
│ │ ├── user_repo.rs
│ │ └── item_repo.rs
│ ├── errors.rs # Error handling
│ └── utils.rs # Utility functions
├── migrations/ # Database migrations
│ ├── 20230101000000_create_users.sql
│ └── 20230101000001_create_items.sql
├── static/ # Static files
│ ├── index.html
│ └── assets/
│ ├── css/
│ ├── js/
│ └── images/
└── tests/ # Integration tests
├── api/
│ ├── auth_tests.rs
│ └── items_tests.rs
└── common/
└── mod.rs
The entry point of your application that initializes and runs the server.
mod config;
mod server;
mod routes;
mod models;
mod handlers;
mod middleware;
mod db;
mod errors;
mod utils;
#[actix_web::main]
async fn main() -> std::io::Result<()> {
// Load environment variables
dotenvy::dotenv().ok();
// Initialize logger
env_logger::init();
// Create database connection pool
let pool = db::connection::init_db().await
.expect("Failed to connect to database");
// Start the server
server::run(pool).await
}
Configures and initializes the HTTP server with middleware and routes.
use actix_web::{App, HttpServer, middleware::Logger};
use actix_files as fs;
use sqlx::PgPool;
pub async fn run(db_pool: PgPool) -> std::io::Result<()> {
let host = std::env::var("HOST").unwrap_or_else(|_| "127.0.0.1".to_string());
let port = std::env::var("PORT").unwrap_or_else(|_| "8080".to_string());
HttpServer::new(move || {
App::new()
.wrap(Logger::default())
.app_data(actix_web::web::Data::new(db_pool.clone()))
.configure(crate::routes::configure)
.service(fs::Files::new("/static", "./static"))
})
.bind(format!("{}:{}", host, port))?
.run()
.await
}
Defines all API endpoints and their corresponding handlers.
use actix_web::web;
use crate::handlers::{auth, items};
use crate::middleware::auth::ApiKeyMiddleware;
pub fn configure(cfg: &mut web::ServiceConfig) {
// Public routes
cfg.service(
web::scope("/api/auth")
.service(auth::login)
.service(auth::register)
);
// Protected routes (require API key)
cfg.service(
web::scope("/api/items")
.wrap(ApiKeyMiddleware)
.service(items::get_items)
.service(items::get_item)
.service(items::create_item)
.service(items::update_item)
.service(items::delete_item)
);
}
Find answers to commonly asked questions about our coding courses.
No prior experience is needed for our beginner courses. We start from the absolute basics and gradually progress to more advanced concepts. For intermediate and advanced courses, we recommend having the prerequisite knowledge mentioned in the course description.
Once you purchase a course, you have lifetime access to all course materials, updates, and the community forum related to that course. We regularly update our content to keep it relevant with the latest industry standards.
Yes, we offer a 30-day money-back guarantee. If you're not completely satisfied with your purchase, you can request a full refund within 30 days of enrollment. No questions asked.
Most courses require about 4-6 hours per week to complete in a reasonable time frame. However, our platform is self-paced, so you can learn according to your own schedule. Each course indicates the estimated completion time in the description.
Yes, all courses come with a certificate of completion that you can add to your resume or LinkedIn profile. For some advanced courses, we also offer industry-recognized certifications upon passing the final assessment.
You'll have access to our community forum where you can ask questions and get help from instructors and fellow students. Premium courses include direct mentor support, code reviews, and weekly live Q&A sessions.
Access our free tutorials, coding challenges, and community projects to supplement your learning.
Browse ResourcesStay updated with the latest programming trends, tips, and industry insights from our expert instructors.
Read Blog