How to Build a Simple API Backend with Rust

Getting Started With Active-Web

We call actix-web a powerful and pragmatic framework.

STEP 1. Getting Started with Hello-World

  1. 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

  2. Add actix-web as a dependency of your project by adding the following to your Cargo.toml file.

    [dependencies] actix-web = "3"

  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
}
  1. Run the application. cargo run

  2. 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
}