How to Build REST APIs with Actix-Web in Rust

MarShell2371 pts0 comments

How to Build REST APIs with Actix-web in Rust

Skip to main content

Sign<br>in<br>Sign up

Close menu

Enterprise

DevOps

SRE

Platform

Pricing

Docs

Request Demo

Support

Sign<br>up

Existing customer?<br>Sign in

How to Build REST APIs with Actix-web in Rust

A practical guide to building high-performance REST APIs in Rust using the Actix-web framework.

@nawazdhandala

Feb 01, 2026

Reading time

Rust

Actix-web

REST API

Web Development

Backend

On this page

Rust has been gaining traction for backend development, and for good reason. Memory safety without garbage collection, fearless concurrency, and performance that rivals C++ make it an attractive choice for building APIs that need to handle serious traffic. Actix-web is the most popular Rust web framework, and it consistently tops benchmarks while remaining pleasant to work with.<br>In this guide, we'll build a complete REST API from scratch. No toy examples - we'll cover everything you need to ship production code: routing, request handling, state management, middleware, serialization, and proper error handling.<br>Setting Up Your Project<br>First, create a new Rust project and add the dependencies we'll need.<br>cargo new rust-api<br>cd rust-apiOpen Cargo.toml and add these dependencies:<br>[package]<br>name = "rust-api"<br>version = "0.1.0"<br>edition = "2021"

[dependencies]<br>actix-web = "4"<br>serde = { version = "1.0", features = ["derive"] }<br>serde_json = "1.0"<br>tokio = { version = "1", features = ["macros", "rt-multi-thread"] }<br>uuid = { version = "1", features = ["v4", "serde"] }<br>chrono = { version = "0.4", features = ["serde"] }<br>env_logger = "0.10"<br>log = "0.4"We're pulling in actix-web for the framework, serde for JSON serialization, uuid for generating IDs, chrono for timestamps, and logging utilities. Run cargo build to fetch everything.<br>Basic Server and Routing<br>Let's start with a minimal server and build from there. This sets up the HTTP server with a single health check endpoint.<br>use actix_web::{get, web, App, HttpResponse, HttpServer, Responder};

// Health check endpoint - useful for load balancers and monitoring<br>#[get("/health")]<br>async fn health_check() -> impl Responder {<br>HttpResponse::Ok().json(serde_json::json!({<br>"status": "healthy",<br>"timestamp": chrono::Utc::now().to_rfc3339()<br>}))

#[actix_web::main]<br>async fn main() -> std::io::Result {<br>// Initialize logging<br>env_logger::init_from_env(env_logger::Env::default().default_filter_or("info"));

log::info!("Starting server at http://127.0.0.1:8080");

HttpServer::new(|| {<br>App::new()<br>.service(health_check)<br>})<br>.bind("127.0.0.1:8080")?<br>.run()<br>.await<br>}The #[get(&quot;/health&quot;)] attribute macro handles routing automatically. You can also use #[post], #[put], #[delete], and #[patch] for other HTTP methods. Run cargo run and hit http://localhost:8080/health to verify it works.<br>Domain Models with Serde<br>Before writing more handlers, let's define our data models. We'll build a simple task management API. Serde handles all the JSON conversion through derive macros.<br>use serde::{Deserialize, Serialize};<br>use uuid::Uuid;<br>use chrono::{DateTime, Utc};

// Task represents a single task in our system<br>// Serialize lets us convert to JSON, Deserialize lets us parse from JSON<br>#[derive(Debug, Clone, Serialize, Deserialize)]<br>pub struct Task {<br>pub id: Uuid,<br>pub title: String,<br>pub description: Option,<br>pub completed: bool,<br>pub created_at: DateTime,<br>pub updated_at: DateTime,

// CreateTask is what clients send when creating a new task<br>// We don't let clients set the ID or timestamps - those are server-controlled<br>#[derive(Debug, Deserialize)]<br>pub struct CreateTask {<br>pub title: String,<br>pub description: Option,

// UpdateTask uses Option for all fields<br>// This allows partial updates where clients only send what they want to change<br>#[derive(Debug, Deserialize)]<br>pub struct UpdateTask {<br>pub title: Option,<br>pub description: Option,<br>pub completed: Option,

impl Task {<br>pub fn new(title: String, description: Option) -> Self {<br>let now = Utc::now();<br>Task {<br>id: Uuid::new_v4(),<br>title,<br>description,<br>completed: false,<br>created_at: now,<br>updated_at: now,<br>}Separating the domain model from request/response DTOs is a pattern that pays off. It keeps your API contract stable even when internal representations change.<br>Application State<br>Real APIs need to store data somewhere. We'll use an in-memory store wrapped in thread-safe primitives. In production, you'd swap this for a database connection pool.<br>use std::collections::HashMap;<br>use std::sync::Mutex;

// AppState holds our application's shared state<br>// Mutex ensures safe concurrent access across threads<br>pub struct AppState {<br>pub tasks: Mutex>,

impl AppState {<br>pub fn new() -> Self {<br>AppState {<br>tasks: Mutex::new(HashMap::new()),<br>}Register the state with your app using app_data. Actix wraps it in an Arc automatically.<br>#[actix_web::main]<br>async fn main() -> std::io::Result {<br>env_logger::init_from_env(env_logger::Env::default().default_filter_or("info"));

// Create shared state that will be available to all handlers<br>let...

rust actix build serde task option

Related Articles