Strong Type Safety
Typed requests and responses make handler contracts explicit.
Batteries Included
Get started fast with an extensive first-party ecosystem.
Trusted in Production
Over 80 million downloads and counting.
Built for Performance
High throughput. Low latency. Tiny memory requirements.
Verified by independent benchmarks.
Hello, World!
Start with a route and a handler. Actix Web matches incoming requests to your routes by path and method, then calls the right handler to build a response.
use actix_web::{get, web, App, HttpServer, Responder};
#[get("/")]
async fn index() -> impl Responder {
"Hello, World!"
}
#[get("/{name}")]
async fn hello(name: web::Path<String>) -> impl Responder {
format!("Hello {}!", &name)
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| App::new().service(index).service(hello))
.bind(("127.0.0.1", 8080))?
.run()
.await
}
use actix_web::{web, App, HttpServer, Responder};
async fn index() -> impl Responder {
"Hello, World!"
}
async fn hello(name: web::Path<String>) -> impl Responder {
format!("Hello {}!", &name)
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.service(web::resource("/").get(index))
.service(web::resource("/{name}").get(hello))
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
Powerful Extractors
Just ask for what you need. Extractors turn request data into ready-to-use types, just by adding a function parameter.
From JSON and query parameters to typed headers and file uploads, the parsing is handled for you. Less boilerplate. Cleaner handlers.
#[derive(Deserialize, Serialize)]
struct Event {
id: i32,
timestamp: f64,
kind: String,
tags: Vec<String>,
}
async fn capture_event(
db: web::Data<DatabaseHandle>,
event: web::Json<Event>,
) -> impl Responder {
let new_event = store_in_db(&db, &event).await;
format!("Received event id={}", new_event.id)
}
#[derive(Deserialize)]
struct Register {
username: String,
country: String,
}
// Accept JSON or URL-encoded form data.
async fn register(
form: Either<web::Json<Register>, web::Form<Register>>,
) -> impl Responder {
let Register { username, country } = form.into_inner();
format!("Hello {username} from {country}!")
}
#[derive(Deserialize)]
pub struct EventFilter {
kind: String,
#[serde(default)]
page: u32,
}
// GET /events?kind=click&page=2
pub async fn list_events(
filter: web::Query<EventFilter>,
) -> impl Responder {
format!("Events of kind {} on page {}", filter.kind, filter.page)
}
// Accept: text/csv;q=0.5, application/json
pub async fn preferred_format(
web::Header(accept): web::Header<Accept>,
) -> impl Responder {
if accept.preference() == mime::TEXT_CSV {
"kind,count\nclick,42\n"
} else {
r#"[["click",42]]"#
}
}
pub async fn profile(auth: BearerAuth) -> Result<String> {
let username = authenticate(auth.token()).await?;
Ok(format!("Welcome, {username}!"))
}
#[derive(MultipartForm)]
pub struct Upload {
title: Text<String>,
#[multipart(limit = "10 MiB")]
file: TempFile,
}
pub async fn upload(
MultipartForm(form): MultipartForm<Upload>,
) -> impl Responder {
format!("Received {} ({} bytes)", form.title.0, form.file.size)
}
Batteries Included
The Actix Web team maintains a whole ecosystem of flexible and high quality crates for building real apps. Get off the ground without reinventing the things you expect to be there.
Add the crates your application needs, with documentation and examples to get you started.
let tls_config = rustls::ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(tls_certs, private_key)
.unwrap();
HttpServer::new(|| {
App::new().route("/", web::get().to(index))
})
.bind_rustls_0_23(("127.0.0.1", 8443), tls_config)?
.run()
.await
pub async fn websocket(
req: HttpRequest,
pl: web::Payload,
) -> Result<HttpResponse, Error> {
let (res, mut session, incoming) = actix_ws::handle(&req, pl)?;
rt::spawn(async move {
session.text("Hello, WebSocket!").await?;
session.close(None).await
});
Ok(res)
}
pub async fn events() -> impl Responder {
let (tx, rx) = mpsc::channel(10);
rt::spawn(async move {
let event = sse::Data::new("Connected!").event("ready");
tx.send(event.into()).await
});
sse::Sse::from_infallible_receiver(rx)
}
App::new().wrap(
Cors::default()
.supports_credentials()
.allowed_origin("https://example.com")
.allowed_methods(["GET", "POST"])
.allowed_header("Content-Type"),
)
async fn index(session: Session) -> Result<HttpResponse, Error> {
// access session data
if let Some(count) = session.get::<i32>("counter")? {
session.insert("counter", count + 1)?;
} else {
session.insert("counter", 1)?;
}
Ok(HttpResponse::Ok().body(format!(
"Count is {:?}!",
session.get::<i32>("counter")?.unwrap()
)))
}
App::new().service(
Files::new("/static", "./static")
.index_file("index.html")
.try_compressed(),
)
#[get("/accounts/{account_id}/status")]
#[tracing::instrument(skip_all, fields(%account_id))]
async fn status(
Path(account_id): Path<u64>,
req_id: RequestId,
) -> HttpResponse {
// Logged as child of request trace and includes the account ID.
tracing::info!("Account status requested");
HttpResponse::Ok()
.insert_header(("x-request-id", req_id.to_string()))
.body("Ready")
}
