---
title: How to Build a REST API with Actix-Web in Rust
summary: Initialize a Rust project, add actix-web, define a handler, and run the server to build a REST API.
lang: en
slug: how-to-build-a-rest-api-with-actix-web-in-rust
published: 2026-04-17T12:05:42+00:00
updated: 2026-04-25T22:00:13+00:00
canonical: https://www.rustfaq.org/en/how-to-build-a-rest-api-with-actix-web-in-rust/
---

# How to Build a REST API with Actix-Web in Rust



## How to Build a REST API with Actix-Web in Rust

You have a service to build. Maybe it's a JSON endpoint that transforms data, or a backend for a frontend you're writing. You tried Python, but the latency is creeping up, or you just want the compiler to catch bugs before they hit production. You pick Actix-Web because the benchmarks look impressive. Now you need to turn that crate into a running server. Here's how you wire it up, why the async machinery works the way it does, and how to structure your routes so the code doesn't collapse as you add endpoints.

## Concept: The kitchen manager

Actix-Web is an async web framework. It manages incoming HTTP connections, matches URLs to your code, and runs your handlers concurrently. Under the hood, it uses an actor model to distribute work across threads, which is why it scales well. For your code, you mostly interact with three pieces. `HttpServer` starts the listener and spawns workers. `App` holds your configuration and routes. Handlers are async functions that take request data and return a response.

Think of Actix-Web as a restaurant kitchen manager. The `HttpServer` is the building. The `App` is the menu and the kitchen layout. The `route` is the sign pointing customers to the right counter. The `handler` is the chef who actually cooks the dish. When a request comes in, the manager checks the menu, finds the right counter, and sends the order to the chef. The chef prepares the response and hands it back.

## Minimal example: Hello world

Start with a project that responds to a single GET request. This example shows the core structure without any extra dependencies.

```rust
use actix_web::{web, App, HttpResponse, HttpServer, Responder};

/// Handles GET requests to "/" and returns plain text.
async fn hello() -> impl Responder {
    // HttpResponse::Ok sets the status code to 200.
    // .body attaches the payload to the response.
    HttpResponse::Ok().body("Hello world!")
}

// The macro sets up the async runtime for Actix.
#[actix_web::main]
async fn main() -> std::io::Result<()> {
    // HttpServer::new takes a closure that builds the App.
    // The closure runs once per worker thread.
    HttpServer::new(|| {
        App::new()
            // web::get() creates a route for GET requests.
            // .to() links the route to the handler function.
            .route("/", web::get().to(hello))
    })
    // Bind to localhost on port 8080.
    // The ? operator propagates binding errors.
    .bind("127.0.0.1:8080")?
    // Start the server and wait for it to finish.
    .run()
    .await
}
```

Run `cargo add actix-web` to pull in the crate. Then run `cargo run`. The server starts and listens on port 8080. Hit `http://127.0.0.1:8080` in your browser or with `curl`. You'll see `Hello world!`.

**Convention aside:** Actix-Web ships with its own `#[actix_web::main]` macro. This isn't just a convenience wrapper. It configures the Tokio runtime with settings optimized for Actix's actor system. Using `#[tokio::main]` directly can lead to subtle performance issues or panics because the runtime isn't set up the way Actix expects. Stick to the Actix macro.

## Walkthrough: From socket to response

When you run the code, `main` executes asynchronously. `HttpServer::new` receives a closure. Actix spawns worker threads based on your CPU cores. Each thread calls your closure once to build an `App` instance. This means if you create state inside the closure, every worker gets its own copy. `bind` opens the TCP socket. If the port is taken, the `?` operator returns the error and the program exits. `run` starts the event loop.

When a browser hits `http://127.0.0.1:8080`, Actix parses the HTTP request, matches the path `/` against your routes, and invokes `hello`. The handler returns a `HttpResponse` implementing `Responder`. Actix converts that into bytes and writes them back to the socket. The whole cycle happens without blocking other requests.

## Realistic example: JSON and path parameters

Real APIs return structured data and accept parameters. This example adds JSON serialization and a path parameter to fetch a user by ID.

```rust
use actix_web::{web, App, HttpResponse, HttpServer, Responder};
use serde::{Deserialize, Serialize};

// Serialize derives the JSON serialization trait.
// Actix uses serde to convert structs to JSON automatically.
#[derive(Serialize)]
struct User {
    id: u32,
    name: String,
}

/// Handles GET /users/{id} and returns a JSON user object.
async fn get_user(path: web::Path<u32>) -> impl Responder {
    // web::Path extracts and parses the URL parameter.
    // into_inner() retrieves the parsed value.
    let user_id = path.into_inner();

    // Construct the response data.
    let user = User {
        id: user_id,
        name: format!("User {}", user_id),
    };

    // .json() serializes the struct to JSON and sets
    // the Content-Type header to application/json.
    HttpResponse::Ok().json(user)
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            // {id} is a path parameter placeholder.
            // Actix matches this pattern and extracts the value.
            .route("/users/{id}", web::get().to(get_user))
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}
```

Add `serde` to your dependencies. Run the server and hit `http://127.0.0.1:8080/users/42`. You'll get `{"id":42,"name":"User 42"}`.

**Convention aside:** Actix-Web keeps its default dependency set small. JSON support isn't enabled by default. Add the `json` feature to `actix-web` in `Cargo.toml` (`actix-web = { version = "4", features = ["json"] }`) and include `serde` for the derive macros. This keeps your compile times down if you're only serving static files or plain text.

## Sharing state across handlers

Real APIs need state. A database connection pool, a cache, or configuration. Actix-Web provides `web::Data` to share immutable state across handlers. `web::Data` is a thread-safe wrapper. You create the state, wrap it in `web::Data::new`, and attach it to the `App`. Handlers extract it by adding a parameter of type `web::Data<State>`.

```rust
use actix_web::{web, App, HttpResponse, HttpServer, Responder};
use std::sync::Mutex;

struct AppState {
    // Mutex allows interior mutability for the shared state.
    // Actix handlers are async and share the Data across threads.
    counter: Mutex<u32>,
}

/// Increments the shared counter and returns the new value.
async fn increment(data: web::Data<AppState>) -> impl Responder {
    // Lock the mutex to modify the counter.
    // This is a blocking operation, but Mutex lock is fast.
    let mut counter = data.counter.lock().unwrap();
    *counter += 1;
    HttpResponse::Ok().body(format!("Count: {}", *counter))
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    // Create the shared state.
    let state = AppState {
        counter: Mutex::new(0),
    };

    HttpServer::new(move || {
        // Clone the Data to move it into the closure.
        // web::Data uses Arc internally, so cloning is cheap.
        let data = web::Data::new(state);
        App::new()
            .app_data(data)
            .route("/increment", web::get().to(increment))
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}
```

**Convention aside:** Cloning `web::Data` is cheap. It wraps an `Arc`. The community convention is to clone the data inside the `HttpServer::new` closure so each worker thread gets a handle to the same shared state. Don't put the `Mutex` inside the `Data` if you can avoid it. Use async locks like `tokio::sync::Mutex` for long operations, or keep the critical section tiny.

## Pitfalls and compiler errors

Actix-Web is fast, but the async model introduces specific traps.

Blocking code freezes the runtime. Actix-Web runs on an async runtime. If your handler calls a blocking function like `std::thread::sleep` or a synchronous database driver, you freeze a worker thread. The runtime can't process other requests on that thread while it waits. Use async-compatible libraries, or wrap blocking calls in `web::block`.

Handlers return `impl Responder`. This is a Rust trait object shorthand. It lets you return different types as long as they implement `Responder`. If you try to return a type that doesn't implement `Responder`, the compiler rejects it with E0277 (trait bound not satisfied). Check that your return type is wrapped in `HttpResponse` or implements the trait.

The closure in `HttpServer::new` runs per worker. If you create a `Vec` or a `DatabaseConnection` inside the closure, each worker gets a separate instance. To share state across workers, create the state before `HttpServer::new` and use `web::Data` to inject it into the app. If you try to move a value into the closure without cloning or wrapping, you get E0382 (use of moved value). The closure is called multiple times, once per worker. You can't move the same value into multiple closures.

Don't block the async runtime. If you must call synchronous code, isolate it behind `web::block` or switch to an async driver.

## Decision: Actix-Web versus alternatives

Rust has several web frameworks. Pick the one that matches your performance needs and your team's comfort level.

Use Actix-Web when you need raw throughput and low latency. It consistently ranks at the top of web framework benchmarks and handles massive concurrency with minimal overhead. Use Axum when you prefer an ergonomic API built on Tower and Hyper. Axum feels more like standard Rust middleware composition and integrates seamlessly with the Tokio ecosystem if you're already using Tower services. Use Warp when you want a filter-based, combinator style for routing. Warp lets you build routes by chaining small filters, which can be expressive for complex validation logic, though it may have higher memory usage than Actix. Use Poem when you want a lightweight framework with a focus on simplicity and a clean API similar to Go's Gin or Python's Flask. Poem is easier to learn for beginners but may not match Actix's peak performance.

Pick the framework that matches your team's comfort level and your performance requirements. Actix-Web wins on speed, but Axum often wins on developer experience.

## Where to go next

- [How to use actix-web crate in Rust web framework](/en/how-to-use-actix-web-crate-in-rust-web-framework/)
- [How to Set Up Routing in Actix-Web](/en/how-to-set-up-routing-in-actix-web/)