Updated Jun 23, 2026

What actix-web Is & Your First Server

You know Rust, and you want to ship something on the web that's fast and won't surprise you in production. That's the corner actix-web has owned for years. It's one of the oldest web frameworks in Rust, and it sits at or near the top of the TechEmpower benchmarks year after year — when people argue about "the fastest web framework," actix-web is usually in the conversation. It's the mature, batteries-included sibling to axum: routing, extractors, middleware, websockets, JSON, all in the box.

The name carries some history. actix-web grew out of an actor framework — that's the "actix" part — and you'll occasionally see the word "actor" in old blog posts and panic about it. Here's the relief: day to day you write plain async fn handlers and never touch an actor. The actor heritage powers some internals, but it almost never surfaces in the code you write. If you've used a web framework in any language, the shape here will feel familiar. (If "web framework" itself is fuzzy — what it buys you over raw sockets — What a Framework Even Is lays the groundwork.)

📝 This guide teaches the framework, not the language. It assumes you're comfortable with Rust — ownership, traits, Result, async/await. actix-web compiles and runs as a normal Rust program, so examples come with the commands to build and run them.

The mental model: three pieces

Before any code, hold one picture in your head. Almost everything in actix-web hangs off these three nouns.

📝 An App holds your routes (and later, shared state). You build it by chaining .route(...) calls. Think of it as the blueprint of your service: "a GET /ping goes here, a POST /articles goes there."

📝 An HttpServer runs copies of that App. It opens the socket, accepts connections, and spreads the work across worker threads — and here's the twist that trips people up later: it builds a separate App per worker. More on that in a moment.

📝 A handler is an async fn whose return value becomes the response, because it returns something that implements the Responder trait. That's actix-web's version of axum's IntoResponse — the return type is the response.

Say it once so it sticks: an App holds routes, an HttpServer runs copies of the App across workers, and each handler is an async fn returning a Responder. That sentence is the spine of every actix-web service you'll ever write.

flowchart LR
  S[HttpServer<br/>runs + spreads load] --> A1["App (worker 1)<br/>holds routes"]
  S --> A2["App (worker 2)<br/>holds routes"]
  A1 --> H["handler<br/>async fn ping()"]
  H --> R["Responder<br/>becomes the response"]

One idea: the server runs many copies of your app at once, and a request flows into one of them, hits the matching handler, and the handler's return value flows back out as the response.

Your first server

First, add the dependency. From inside your Cargo project:

cargo add actix-web

What just happened: cargo add actix-web pulls in the framework and writes it into your Cargo.toml under [dependencies]. Notice there's no separate cargo add tokio line like axum needs — actix-web ships its own runtime (built on top of Tokio) and re-exports the macro you need. One crate, and you're ready.

Now the smallest server that does something real. Put this in src/main.rs:

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

async fn ping() -> impl Responder {
    HttpResponse::Ok().body("pong")
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new().route("/ping", web::get().to(ping))
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

What just happened: walk it from the top —

  • async fn ping() -> impl Responder is the handler. It takes no arguments and returns HttpResponse::Ok().body("pong") — a 200 OK with the text pong as the body. The return type is impl Responder: "some type that knows how to become a response." HttpResponse is one such type, so actix-web accepts it. (Phase 3 explores everything else that's a Responder.)
  • #[actix_web::main] is the one macro you'll use. Rust's main can't normally be async, so this rewrites your async fn main to start actix-web's runtime and run it there. It's the actix-web counterpart to axum's #[tokio::main].
  • HttpServer::new(|| { ... }) takes a closure that builds an App. Inside it, App::new().route("/ping", web::get().to(ping)) creates a fresh app and registers one route: a GET request to /ping runs the ping handler. Read web::get().to(ping) as "for GET, call ping." There's a web::post(), web::put(), web::delete(), and so on for the other methods.
  • .bind(("127.0.0.1", 8080))? opens a socket on port 8080. Binding can fail (port in use, no permission), so it returns a Result — the ? propagates the error up out of main, which is why main returns std::io::Result<()>.
  • .run().await starts the accept loop and runs forever, handing each request to a worker's App. It blocks here until you stop the program.

Build and run it like any Rust binary:

cargo run

actix-web prints a couple of startup lines and then waits for requests. Leave it running, and in another terminal hit the route:

curl 127.0.0.1:8080/ping
$ curl 127.0.0.1:8080/ping
pong

What just happened: curl sent a GET /ping. The server routed it into one of its worker Apps, matched the route to your ping handler, called it, and the handler's returned HttpResponse came back as the response body — pong. You have a working, multi-threaded HTTP server in about a dozen lines.

The catch worth flagging now: the closure runs per worker

Look again at HttpServer::new(|| { App::new()... }). That closure isn't called once. actix-web spawns multiple worker threads (by default, one per CPU core), and it calls your closure once on each worker to build that worker's own App. You end up with several independent Apps running side by side.

⚠️ This is fine for the toy above — building a couple of routes a few times costs nothing. But the moment you want shared state (a database pool, a cache, a counter), the naive move of creating it inside the closure backfires: each worker would get its own separate copy, and they'd never see each other's data. That's why state in actix-web goes through a special wrapper instead. You don't need it yet — just file away the shape of the problem. Phase 4: Shared State with web::Data solves it properly with web::Data<T>.

The running example: an articles API

We won't keep returning pong. Across this guide we'll grow one real service — a small articles API — and the core of it is a single type: an article with an id, a title, and a body.

struct Article {
    id: u32,
    title: String,
    body: String,
}

What just happened: we declared the Article struct the rest of the guide builds on. Right now it's a plain struct with three fields. In Phase 3 we'll derive Serialize and Deserialize on it so actix-web can turn it into JSON on the way out and parse it from a request body on the way in — that's the moment a plain struct becomes a real API resource. For now you've met the whole cast: an App of routes, an HttpServer that runs it across workers, a handler returning a Responder, and the Article we'll spend the next phases turning into a proper REST API.

Next up: routing in earnest — multiple methods, scopes for grouping routes, and your first extractors (Path, Query, Json) that pull pieces out of the incoming request.

Recap

  • actix-web is the mature, fast, batteries-included Rust framework — a perennial TechEmpower leader and the closest peer to axum. Its actor heritage powers internals but rarely shows up in your code; you write plain async fn handlers.
  • The mental model is one sentence: an App holds routes, an HttpServer runs copies of the App across worker threads, and a handler is an async fn returning a Responder.
  • A first server is small: cargo add actix-web (no separate Tokio install — actix-web bundles its own runtime), build the App inside HttpServer::new(|| ...), register web::get().to(handler), then .bind(...)?.run().await. #[actix_web::main] lets main be async.
  • HttpServer::new takes a closure called once per worker. Each worker builds its own App, which is why shared state can't just live inside that closure — web::Data handles it in Phase 4.
  • Run with cargo run, test with curl. The handler's returned HttpResponse is exactly what the client receives.
  • The throughline: an App of routes, run by an HttpServer, with handlers that return a Responder. We grow one articles API along that arrow for the rest of the guide.

Quick check

Three questions on the ideas that have to stick — the three-piece model, what the handler returns, and the per-worker closure:

[
  {
    "q": "In actix-web's mental model, what is the relationship between App and HttpServer?",
    "choices": [
      "App holds the routes; HttpServer runs copies of that App across worker threads",
      "HttpServer holds the routes; App runs them on a single thread",
      "They are the same type with two names",
      "App is the database and HttpServer is the cache"
    ],
    "answer": 0,
    "explain": "An App is the blueprint of routes (and later state). An HttpServer opens the socket and runs copies of that App across worker threads, handing each request to one of them."
  },
  {
    "q": "What does the `ping` handler's return value become, and why is its type `impl Responder`?",
    "choices": [
      "The HTTP response, because any type implementing Responder knows how to become one",
      "A log line printed to the server console",
      "An argument passed to the next handler",
      "Nothing — you must call a separate send() function"
    ],
    "answer": 0,
    "explain": "A handler's return value is the response. `impl Responder` means 'some type that implements the Responder trait', so actix-web knows how to turn it into a full HTTP response. HttpResponse is one such type."
  },
  {
    "q": "Why does `HttpServer::new` take a closure rather than a single built App?",
    "choices": [
      "Because the closure is called once per worker thread, so each worker builds its own App",
      "Because closures are faster to compile than structs",
      "Because the App must be rebuilt on every incoming request",
      "Because Rust forbids passing a struct to a function"
    ],
    "answer": 0,
    "explain": "actix-web spawns multiple workers and calls the closure once on each to build that worker's own App. That's why shared state can't live inside the closure — each worker would get a separate copy. Phase 4's web::Data solves it."
  }
]

Guide overview · Phase 2: Routing & Extractors →

Check your understanding

1. In actix-web's mental model, what is the relationship between App and HttpServer?

2. What does the `ping` handler's return value become, and why is its type `impl Responder`?

3. Why does `HttpServer::new` take a closure rather than a single built App?

Was this page helpful?