Build a Rust Backend API Server
Full Step-by-Step Tutorial

Build a Rust Backend API Server

Serve Static Files, Handle APIs, Add Security & Connect to a Database — All in Rust

🚀

Rust Backend

Fast, reliable server with Actix-web

📄

Static Content

Serve HTML & JSON files

🔁

CRUD API

Complete RESTful operations

🔒

Security

API Keys & JWT authentication

🛢️

Database

Connect with SQLx & Postgres

cargo new rust_api_server
cd rust_api_server
cargo add actix-web && more ...

Overview

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.

Rust Backend Server

Create a fast, secure HTTP server using Actix-web framework

Static Content Delivery

Serve HTML and JSON data efficiently with proper content types

Complete CRUD Operations

Implement GET, POST, PATCH, DELETE endpoints with proper request handling

Multi-layer Security

Implement API Key validation and JWT authentication for secure access

Database Integration

Connect to PostgreSQL/MySQL using SQLx with async queries and connection pooling

What You'll Learn in This Tutorial

01

Project Setup

Installing Rust, creating a new project, and adding required dependencies

02

API Implementation

Building routes, handlers, and implementing complete CRUD operations

03

Security & Database

Adding authentication layers and connecting to a database with async queries

Setup

Get your project environment ready with these simple steps.

terminal
# Install Rust using rustup

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# Create a new project

cargo new rust_api_server

cd rust_api_server

# Add dependencies to Cargo.toml

[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"

Requirements

  • Rust (latest stable version)
  • Cargo (Rust's package manager, installed with rustup)
  • PostgreSQL (for database functionality)
  • Basic understanding of Rust syntax

Project Structure

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

Next Steps

1

Create the project structure

Set up files and directories according to the structure above

2

Set up environment variables

Create .env file with database connection string and API keys

3

Proceed to serving static content

Learn how to serve HTML and JSON files through your API

Static Content

Learn how to serve HTML files and static JSON data from your Rust API server.

Serving Static Files

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
}
1

Create Directory Structure

First, create a "static" directory in your project root to store your static files

2

Add HTML Files

Create an index.html file in the static directory with your content

3

Configure Routes

Use Files::new() to map a URL path to your static directory

4

Access Files

Files will be accessible at http://localhost:8080/static/index.html

Serving JSON Data

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.

CRUD

Implement complete RESTful API endpoints with Create, Read, Update, and Delete operations.

Understanding CRUD Operations

CRUD represents the four basic operations that can be performed on any data:

Create

POST requests to add new resources

Read

GET requests to retrieve resources

Update

PUT/PATCH requests to modify resources

Delete

DELETE requests to remove resources

Complete CRUD Implementation

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
}

Implementing CRUD in Rust

Data Model

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 - List All Items

#[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} - Get Single Item

#[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 - Create New Item

#[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} - Update 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} - Remove Item

#[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")
    }
}

Implementation Notes

  • We're using an in-memory store for this example; later we'll connect to a database
  • Proper error handling should be implemented in production code
  • Consider implementing validation for request data
  • For larger applications, organize handlers into separate modules

Why RESTful APIs?

  • Stateless architecture simplifies server implementation
  • Scalable design pattern for web applications
  • Uniform interface makes APIs predictable and easy to use

Security

Protect your API with multiple layers of authentication and authorization.

API Key Authentication

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"))
    }
}
1

Register Middleware

Add your middleware to the application in main.rs

App::new().wrap(middleware::ApiKey)
2

Set Environment Variable

Store your API key securely as an environment variable

API_KEY=your_secret_api_key
3

Client Usage

Clients must include the API key in request headers

curl -H "x-api-key: your_secret_api_key" http://localhost:8080/api/items

Route Protection

Protect sensitive routes with middleware that validates authentication before allowing access.

Role-Based Access

Implement role-based authorization to control what actions different users can perform on your API.

Password Hashing

Securely store user passwords using Rust's bcrypt or argon2 crates for strong cryptographic hashing.

Rate Limiting

Protect your API from abuse by implementing rate limiting to restrict the number of requests from a single source.

JWT Authentication

Implement secure, stateless token-based authentication with JSON Web Tokens.

JWT Authentication Flow

1. User Login

User submits credentials and receives a token

2. Store Token

Client stores the JWT in local storage or cookies

3. Authorize Requests

Client sends JWT with each request to protected routes

Security Best Practices
  • Keep JWT secrets secure and out of version control
  • Set reasonable expiration times for tokens
  • Use HTTPS to prevent token interception
  • Consider implementing token refresh mechanism

Project Structure

Organize your Rust API server with a clean, maintainable folder structure.

Why Good Structure Matters

  • Maintainability:Well-organized code is easier to maintain and extend
  • Scalability:Structured code accommodates growth without becoming unwieldy
  • Collaboration: Team members can understand and navigate the codebase easily
  • Testability: Modular structure facilitates unit and integration testing

Key Principles to Follow

Separation of Concerns

Each module has a single responsibility and well-defined boundaries

Domain-Driven Design

Structure reflects your domain entities and their relationships

Modularity

Code is organized in modules that can be developed and tested independently

Consistency

Follow the same patterns and naming conventions throughout the codebase

Pro Tips

  • Use Rust's module system to your advantage by creating clear boundaries
  • Follow the Rust naming conventions consistently
  • Prefer small, focused files over large monolithic ones
  • Keep your main.rsclean and delegate to other modules
  • Use feature flags in Cargo.tomlfor optional components

Rust API Project Structure

The 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

Core Files Explained

main.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
}

server.rs

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
}

routes.rs

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)
    );
}
Current LessonNext Lessonarrow_forward

FAQ

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.

Still Have Questions?

Learning Resources

Access our free tutorials, coding challenges, and community projects to supplement your learning.

Browse Resources

Blog & Tech News

Stay updated with the latest programming trends, tips, and industry insights from our expert instructors.

Read Blog