How to use actix-web crate in Rust web framework

Install actix-web via Cargo and define an async handler function to route HTTP requests.

The first request

You have a Rust program that calculates something useful. Now you want a browser to trigger it. You open a terminal, type curl http://localhost:8080, and expect a response. The Rust standard library does not include an HTTP server. It gives you threads, files, and basic networking primitives, but parsing HTTP headers, managing keep-alive connections, and routing paths requires a framework.

Actix-web is one of the most mature choices in the ecosystem. It handles the socket, parses the request, matches the path, and hands you the data. You write a function that returns a response. The framework does the rest.

How actix-web actually works

Actix-web is built on the actor model. The name comes from the original actix crate, which provides a messaging system for concurrent tasks. Think of it like a restaurant kitchen. The HttpServer is the host standing at the door. The App is the kitchen layout with stations and counters. Routes are the menu items. Handlers are the chefs.

When a request arrives, the host checks the menu. It finds the matching route and hands the order to the correct chef. The chef cooks it asynchronously, meaning they can start a dish, step away to prep another, and come back when the oven dings. Actix-web manages the waiters, the queue, and the thread pool so you never have to manually spawn threads or worry about blocking the kitchen.

The framework uses a builder pattern to configure everything. You chain methods to add routes, middleware, and state. Each method returns a modified builder. When you call .run(), the builder compiles into a running server. This pattern keeps configuration explicit and prevents partial setups.

The smallest working server

Start by adding the dependency to your manifest. Actix-web 4 requires Rust 1.65 or newer and pulls in the necessary async runtime automatically.

[dependencies]
actix-web = "4"

Create a main.rs with the following structure. The code sets up a single route that responds to GET requests at the root path.

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

// The handler runs when a request matches the route.
// It returns a type that implements Responder, which tells actix how to format the output.
async fn hello() -> impl Responder {
    HttpResponse::Ok().body("Hello world!")
}

// This macro initializes the actix runtime and replaces #[tokio::main].
// Using the wrong macro causes a runtime panic because the executor isn't started.
#[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, so keep it cheap.
    HttpServer::new(|| {
        App::new()
            .route("/", web::get().to(hello))
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

Run cargo run. Open http://127.0.0.1:8080 in a browser. You see the text. The server is alive.

What happens under the hood

When main executes, #[actix_web::main] spins up an async executor. The executor is a thread pool that runs futures to completion. It replaces the standard main function and provides the runtime context that all actix components depend on.

HttpServer::new receives a closure. That closure runs once for each worker thread the server spawns. By default, actix uses one thread per CPU core. The closure returns an App instance. App::new() creates an empty application builder. .route("/", web::get().to(hello)) attaches a handler to the root path for the GET method. The web::get() call creates a route builder. .to(hello) registers your function as the endpoint.

.bind("127.0.0.1:8080") opens a TCP socket on localhost port 8080. It returns a Result because the port might already be in use. The ? operator propagates the error to main, which exits gracefully. .run() starts the event loop. It returns a future that resolves when the server shuts down. .await hands control to the executor, which blocks until a signal tells the server to stop.

When a client connects, the executor wakes up a worker. The worker reads bytes from the socket, parses the HTTP request, and matches the path against your routes. It finds /, sees the GET method matches, and calls hello(). The function returns HttpResponse::Ok().body("Hello world!"). The Responder trait converts that response into raw bytes. The worker writes the bytes back to the socket and waits for the next request.

Keep the closure in HttpServer::new lightweight. It runs multiple times. Expensive initialization belongs outside the closure or in a shared state container.

A handler that does real work

Static strings are fine for testing. Real applications need to extract data from the request and return structured payloads. Actix-web provides extractor types that pull values out of the request automatically.

use actix_web::{web, App, HttpResponse, HttpServer, Responder, Result};
use serde::Serialize;

// Define a response structure. Serde handles the JSON conversion.
#[derive(Serialize)]
struct Greeting {
    message: String,
    target: String,
}

// web::Path extracts URL parameters. The type must implement FromRequest.
// web::Json extracts and deserializes the request body.
async fn greet(
    path: web::Path<String>,
    body: web::Json<Greeting>,
) -> Result<impl Responder> {
    // path.into_inner() unwraps the extractor to get the raw String.
    // body.into_inner() does the same for the JSON payload.
    let target = path.into_inner();
    let msg = body.into_inner().message;

    // HttpResponse::Ok().json() serializes the struct and sets the correct content type.
    // This is the standard convention for returning JSON in actix-web.
    Ok(HttpResponse::Ok().json(Greeting {
        message: format!("Received: {}", msg),
        target,
    }))
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            // The {name} placeholder matches the web::Path extractor.
            .route("/greet/{name}", web::post().to(greet))
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

Send a POST request with a JSON body and a path parameter. The framework parses both, hands them to your function, and returns a JSON response. The Result return type lets you return errors that actix converts to HTTP 500 responses automatically.

Extractors fail gracefully. If the client sends malformed JSON, web::Json returns a 400 Bad Request before your function runs. If the path does not match the type, you get a 404. You do not need to write validation boilerplate for basic routing.

Where beginners trip up

The async runtime is the first wall. Actix-web ships with its own runtime initialization macro. If you use #[tokio::main] or #[async_std::main] instead, the server panics at startup. The error reads there is no reactor running, must be called from the context of a Tokio 1.x runtime. The fix is simple. Replace the macro with #[actix_web::main]. The macro configures the executor, sets up the signal handlers, and registers the actix system.

Blocking the executor is the second wall. Async functions should yield control frequently. If you run a CPU-heavy loop or call a synchronous blocking API, you freeze the worker thread. Other requests queue up. The server appears to hang. Actix provides web::block to offload synchronous work to a separate thread pool. Wrap blocking calls in web::block(|| { /* heavy work */ }).await. The executor moves the task to the blocking pool and resumes when it finishes.

Trait bound errors are the third wall. If you return a type that does not implement Responder, the compiler rejects you with E0277 (the trait bound YourType: Responder is not satisfied). This happens when you return a raw String, a Vec, or a custom struct without wrapping it. The compiler tells you exactly which trait is missing. Wrap the value in HttpResponse::Ok().body(), HttpResponse::Ok().json(), or implement Responder yourself. The error message points to the exact line. Follow it.

Convention matters here. The community expects actix-web::main for standalone servers. They expect HttpResponse::Ok().json() for JSON endpoints. They expect web::block for synchronous dependencies. Following these patterns makes your code readable to anyone who has touched actix before.

When to pick actix-web

Use actix-web when you need high throughput and fine-grained control over the request lifecycle. Use actix-web when your project already depends on the actix ecosystem for websockets, streaming, or background actors. Use actix-web when you want a mature, battle-tested framework with extensive middleware support. Reach for axum when you prefer a hyper-based stack that aligns closely with Tokio and tower ecosystem conventions. Reach for rocket when you want declarative routing macros and automatic request extraction with minimal boilerplate. Reach for plain hyper when you are building a custom protocol or need absolute control over the HTTP parser without framework abstractions.

Actix-web trades some ergonomic simplicity for performance and flexibility. The builder pattern and trait-based extractors require reading the documentation once. The payoff is a server that scales predictably under load.

Where to go next