We call actix-web a powerful and pragmatic framework.
STEP 1. Getting Started with Hello-World
Actix Web currently has a minimum supported Rust version (MSRV) of 1.42. Running rustup update will ensure you have the latest and greatest Rust version available
$ rustup update $ cargo new hello-world $ cd hello-world
Add actix-web as a dependency of your project by adding the following to your Cargo.toml file.
[dependencies] actix-web = "3"
Modifyl src/main.rs
use actix_web::{get, post, web, App, HttpResponse, HttpServer, Responder};
#[get("/")]
async fn hello() -> impl Responder {
HttpResponse::Ok().body("Hello world!")
}
#[post("/echo")]
async fn echo(req_body: String) -> impl Responder {
HttpResponse::Ok().body(req_body)
}
async fn manual_hello() -> impl Responder {
HttpResponse::Ok().body("Hey there!")
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.service(hello)
.service(echo)
.route("/hey", web::get().to(manual_hello))
})
.bind("127.0.0.1:8080")?
.run()
.await
}
Run the application.
cargo run
Test API
curl http://localhost:8080
```curl http://localhost:8080/hey```
curl -H "Content-Type: application/json" \
-d '{"username":"fred","password":"passe"}' \
http://localhost:8080/echo
STEP 2. Actix-Web Application
actix-web provides various primitives to build web servers and applications with Rust. It provides routing, middleware, pre-processing of requests, post-processing of responses, etc.
All actix-web servers are built around the App instance. It is used for registering routes for resources and middleware. It also stores application state shared across all handlers within the same scope.
An application’s scope
acts as a namespace for all routes, i.e. all routes for a specific application scope have the same url path prefix. The application prefix always contains a leading “/” slash. If a supplied prefix does not contain leading slash, it is automatically inserted. The prefix should consist of value path segments.
use actix_web::{get, web, App, HttpServer};
use std::sync::Mutex;
// This struct represents state
struct AppState {
app_name: String,
counter: Mutex<i32>,
}
#[get("/")]
async fn index(data: web::Data<AppState>) -> String {
let app_name = &data.app_name; // <- get app_name
let mut counter = data.counter.lock().unwrap(); // <- get counter's MutexGuard
*counter += 1; // <- access counter inside MutexGuard
format!("\r\nHello {}!\r\nRequest number: {}\r\n", app_name, counter) // <- response with app_name
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let app_state = web::Data::new(AppState {
counter: Mutex::new(0),
app_name: String::from("Actix-Web App"),
});
HttpServer::new(move || {
App::new()
.app_data(app_state.clone())
.service(index)
})
.bind("127.0.0.1:8080")?
.run()
.await
}
Step 3: Application guards and virtual hosting
The web::scope()
method allows setting a resource group prefix. This scope represents a resource prefix that will be prepended to all resource patterns added by the resource configuration. This can be used to help mount a set of routes at a different location than the original author intended while still maintaining the same resource names.
You can think of a guard
as a simple function that accepts a request object reference and returns true or false. Formally, a guard is any object that implements the Guard trait. Actix-web provides several guards. One of the provided guards is Header
. It can be used as a filter based on request header information.
Step4: The HTTP Server
The HttpServer
type is responsible for serving HTTP requests.
HttpServer accepts an application factory as a parameter, and the application factory must have Send + Sync boundaries. More about that in the multi-threading section.
To bind to a specific socket address, bind()
must be used, and it may be called multiple times. To bind ssl socket, bind_openssl()
or bind_rustls()
should be used. To run the HTTP server, use the HttpServer::run()
method.
step5: Request Handlers
A request handler is an async function that accepts zero or more parameters that can be extracted from a request and returns a type that can be converted into an HttpResponse.
Request handling happens in two stages. First the handler object is called, returning any object that implements the Responder trait. Then, respond_to()
is called on the returned object, converting itself to a HttpResponse or Error.
By default actix-web provides Responder implementations for some standard types, such as &'static str
, String
, etc.
async fn index(_req: HttpRequest) -> &'static str {
"Hello world!"
}
async fn index(_req: HttpRequest) -> String {
"Hello world!".to_owned()
}
To return a custom type directly from a handler function, the type needs to implement the Responder trait.
Step6: Type-safe information extraction
Actix-web provides a facility for type-safe request information access called extractors. By default, actix-web provides several extractor implementations. An extractor can be accessed as an argument to a handler function. Actix-web supports up to 12 extractors per handler function. Argument position does not matter.
web::Path
Path provides information that can be extracted from the Request’s path. You can deserialize any variable segment from the path.web::Query
Query type provides extraction functionality for the request’s query parameters. Underneath it uses serde_urlencoded crate.web::Json
Json allows deserialization of a request body into a struct. To extract typed information from a request’s body, the type T must implement the Deserialize trait from serde.web::Data
Data - If you need access to an application state.web::HttpRequest
HttpRequest - HttpRequest itself is an extractor which returns self, in case you need access to the request.- String - You can convert a request’s payload to a String.
- Bytes - You can convert a request’s payload into Bytes.
- Payload - You can access a request’s payload. Example
async fn index(path: web::Path<(String, String)>, json: web::Json<MyInfo>) -> impl Responder {
let path = path.into_inner();
format!("{} {} {} {}", path.0, path.1, json.id, json.username)
}
Path
For instance, for resource that registered for the /users/{user_id}/{friend}
path, two segments could be deserialized, user_id and friend. These segments could be extracted into a tuple
, i.e. Path<(u32, String)>
or any structure that implements the Deserialize
trait from the serde crate.
It is also possible to extract path information to a specific type that implements the Deserialize trait from serde. Here is an equivalent example that uses serde instead of a tuple type.
use actix_web::{get, web, Result};
use serde::Deserialize;
#[derive(Deserialize)]
struct Info {
user_id: u32,
friend: String,
}
/// extract path info using serde
#[get("/users/{user_id}/{friend}")] // <- define path parameters
async fn index(info: web::Path<Info>) -> Result<String> {
Ok(format!(
"Welcome {}, user_id {}!",
info.friend, info.user_id
))
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
use actix_web::{App, HttpServer};
HttpServer::new(|| App::new().service(index))
.bind("127.0.0.1:8080")?
.run()
.await
}
It is also possible to extract path information to a specific type that implements the Deserialize trait from serde.
It is also possible to get or query the request for path parameters by name.
#[get("/users/{userid}/{friend}")] // <- define path parameters
async fn index(req: HttpRequest) -> Result<String> {
let name: String = req.match_info().get("friend").unwrap().parse().unwrap();
let userid: i32 = req.match_info().query("userid").parse().unwrap();
Ok(format!("Welcome {}, userid {}!", name, userid))
}
Json
#[derive(Deserialize)]
struct Info {
username: String,
}
#[get("/")]
async fn index(info: web::Json<Info>) -> Result<String> {
Ok(format!("Welcome {}!", info.username))
}
Form
At the moment, only url-encoded forms are supported. The url-encoded body could be extracted to a specific type. This type must implement the Deserialize trait from the serde crate.
#[derive(Deserialize)]
struct FormData {
username: String,
}
#[post("/")]
async fn index(form: web::Form<FormData>) -> Result<String> {
Ok(format!("Welcome {}!", form.username))
}
Query
#[derive(Deserialize)]
struct Info {
username: String,
}
#[get("/")]
async fn index(info: web::Query<Info>) -> String {
format!("Welcome {}!", info.username)
}
Data
Application state is accessible from the handler with the web::Data extractor; however, state is accessible as a read-only reference. If you need mutable access to state, it must be implemented.
Beware, actix creates multiple copies of the application state and the handlers but one copy for each thread. For example, To count the number of total requests across all threads, one should use Arc and atomics.
use actix_web::{get, web, App, HttpServer, Responder};
use std::cell::Cell;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
#[derive(Clone)]
struct AppState {
local_count: Cell<usize>,
global_count: Arc<AtomicUsize>,
}
#[get("/")]
async fn show_count(data: web::Data<AppState>) -> impl Responder {
format!(
"\r\nglobal_count: {}\r\nlocal_count: {}\r\n",
data.global_count.load(Ordering::Relaxed),
data.local_count.get()
)
}
#[get("/add")]
async fn add_one(data: web::Data<AppState>) -> impl Responder {
data.global_count.fetch_add(1, Ordering::Relaxed);
let local_count = data.local_count.get();
data.local_count.set(local_count + 1);
format!(
"\r\nglobal_count: {}\r\nlocal_count: {}\r\n",
data.global_count.load(Ordering::Relaxed),
data.local_count.get()
)
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let data = AppState {
local_count: Cell::new(0),
global_count: Arc::new(AtomicUsize::new(0)),
};
HttpServer::new(move || {
App::new()
.data(data.clone())
.service(show_count)
.service(add_one)
})
.bind("127.0.0.1:8080")?
.run()
.await
}