Getting Started with Actix Web - The Battle-tested Rust Framework

shuttle_dev

Shuttle

Posted on December 15, 2023

Getting Started with Actix Web - The Battle-tested Rust Framework

To this day, actix-web remains an extremely formidable competitor in the Rust web backend ecosystem. It's no wonder that it's lasted so long; despite any impact that previous events may have had on it, it's still going strong and is one of the most recommended web frameworks in Rust. Originally based on the actor framework of the same name (actix), it has since moved away, and actix is now only really used for websocket endpoints.

This article will primarily be concerning v4.4.0.

Getting Started with Actix Web

To get started, you want to use cargo init example-api to generate your project, cd into the folder, and then use the following to install actix-web:

cargo add actix-web
Enter fullscreen mode Exit fullscreen mode

Now you're pretty much ready to start! If you'd like to copy the boilerplate for getting straight to writing your app, here it is:

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

#[get("/")]
async fn index() -> impl Responder {
    "Hello world!"
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new().service(
            // prefixes all resources and routes attached to it...
            web::scope("/")
                // ...so this handles requests for `GET /app/index.html`
                .route("/", web::get().to(index)),
        )
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}
Enter fullscreen mode Exit fullscreen mode

Routing in Actix Web

When using actix-web, mostly any function that returns the actix_web::Responder trait can be used as a route. See below for a basic Hello World example:

#[get("/")]
async fn index() -> impl Responder {
    "Hello world!"
}
Enter fullscreen mode Exit fullscreen mode

This handler function can then be fed into an actix_web::App which then gets passed in as a parameter to a HttpServer:

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new().service(
            // prefixes all resources and routes attached to it...
            web::scope("/")
                // ...so this handles requests for the base route
                .route("/index.html", web::get().to(index)),
        )
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}
Enter fullscreen mode Exit fullscreen mode

Now whenever you go to /index.html, it should return "Hello world!". However, you may find this approach a little bit lacking if you want to create multiple mini-router types and then merge them all into the app at the end. In this case, you will want the actix_web::ServiceCfg struct, which you can also write like this:

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

// this function could be located in different module
fn config(cfg: &mut web::ServiceConfig) {
    cfg.service(web::resource("/test")
        .route(web::get().to(|| HttpResponse::Ok()))
        .route(web::head().to(|| HttpResponse::MethodNotAllowed()))
    );
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new().configure(config)
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}
Enter fullscreen mode Exit fullscreen mode

Extractors in Actix Web are exactly that: type-safe request implementations that, when passed into a handler function, will attempt to extract the relevant data from the request for the handler function. For example, the actix_web::web::Json extractor will attempt to extract JSON from the request body. To successfully deserialize JSON successfully however, you need to use the serde crate - ideally with the derive function that adds automatic Deserialize and Serialize derive macros for your structs. You can install serde by executing the following command:

cargo install serde -F derive
Enter fullscreen mode Exit fullscreen mode

Now you can use it as a derive macro like this:

use actix_web::{web, Result};
use serde::Deserialize;

#[derive(Deserialize)]
struct Info {
    username: String,
}

// deserialize `Info` from request's body
#[post("/submit")]
async fn submit(info: web::Json<Info>) -> String {
    format!("Welcome {}!", info.username) 
}
Enter fullscreen mode Exit fullscreen mode

Actix Web also has support for paths, queries and forms. You'll also need to use serde here as well - although with paths, you will additionally want to use the Actix Web routing macro to declare where exactly the path parameters are. We can find examples of all 3 of these in action below:

#[derive(Deserialize)]
struct Info {
    username: String,
}

// extract path info using serde
#[get("/users/{username}")] // <- define path parameters
async fn index(info: web::Path<Info>) -> String {
    format!("Welcome {}!", info.username)
}

// data is passed in here through query parameters in the URL
// for example, google.com/?hello=world
#[get("/")]
async fn index(info: web::Query<Info>) -> String {
    format!("Welcome {}!", info.username)
}

// data is passed into the Form extractor through a HTML Form element
#[post("/")]
async fn index(form: web::Form<Info>) -> String {
    Ok(format!("Welcome {}!", form.username))
}
Enter fullscreen mode Exit fullscreen mode

Interested in writing your own extractor? You can do that! Writing your own extractor simply requires that you implement the FromRequest trait. You will also need to Check out this code for a HTTP header extractor that shows you exactly how it's done:

use actix_web::dev::Payload;
use actix_web::{FromRequest, 
    http::header::Header as ParseHeader,
    HttpRequest, error::ParseError
};

#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct Header<T>(pub T);

impl<T> FromRequest for Header<T>
where
    T: ParseHeader,
{
    type Error = ParseError;
    type Future = Ready<Result<Self, Self::Error>>;

    #[inline]
    fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
        match ParseHeader::parse(req) {
            Ok(header) => ok(Header(header)),
            Err(e) => err(e),
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Note that the T: ParseHeader trait bound is specific to this trait implementation because in order for a header to be a valid header, it needs to be able to be parsed successfully as a header, with the error implementing actix_web::error::Error. Although we also have extract as a provided method, from_request is the only required method to be implemented here, which returns Self::Future. That is to say, we need to return a result that's ready to be awaited - you can find more about the Ready struct here. Other extractors, like the JSON extractor, also allow you to change their configuration - you can find out more about it in this docs page.

Generally speaking, responses only need to implement the actix_web::Responder trait to be able to respond. Although there is a broad range of implementations already when it comes to actual response types so you should not generally need to implement your own types, there can be specific use cases where this is helpful; for example, being able to document all the types of responses that your application may have.

Adding a Database

Normally when setting up a database, you might need to set up your database connection:

use sqlx::PgPoolOptions;

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    let dbconnection = PgPoolOptions::new()
        .max_connections(5)
        .connect(r#"<db-connection-string-here>"#).await;

    //... rest of your code
}
Enter fullscreen mode Exit fullscreen mode

You would then need to provision your own Postgres instance, whether installed locally on your computer, provisioned through Docker or something else. However, with Shuttle we can eliminate this as the runtime provisions the database for you:

use actix_web::{get, web::ServiceConfig};
use shuttle_actix_web::ShuttleActixWeb;

#[shuttle_runtime::main]
async fn actixweb(
    #[shuttle_shared_db::Postgres] pool: PgPool,
) -> ShuttleActixWeb<impl FnOnce(&mut ServiceConfig) + Send + Clone + 'static> {
    let state = AppState { pool };

    // .. the rest of your code
}
Enter fullscreen mode Exit fullscreen mode

Locally this is done through Docker, but in deployment there is an overarching process that does this for you! No extra work required. We also have an AWS RDS database offering that requires zero AWS knowledge to set up - visit here to find out more.

App State in Actix Web

Routing is great and all (as well as adding databases being pretty easy!) but when you need to store variables, you may be wanting to look for something that lets you store and use them across your application. This is where shared mutable state comes in: you declare it while building your service across your whole application, then you can use it as an extractor in your handler functions. For example, you might need to share a database pool, a counter or a shared hashmap of websocket subscribers. You can declare and use state like this:

use sqlx::PgPool;

#[derive(Clone)]
struct AppState {
    db: PgPool
}

#[get("/")]
async fn index(data: web::Data<AppState>) -> String {
    let res = sqlx::query("SELECT 'Hello World!'").fetch_all(&data.db).await.unwrap();

    format!("{res}") 
}
Enter fullscreen mode Exit fullscreen mode

You can then implement it like this:

#[actix_web::main]
async fn main() -> std::io::Result<()> { 
    let db = connect_to_db();
    let state = web::Data::new(AppState { db });

    HttpServer::new(move || {
        // move app state into the closure
        App::new()
            .app_data(state.clone()) // <- register the created data
            .route("/", web::get().to(index))
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}
Enter fullscreen mode Exit fullscreen mode

Middleware in Actix Web

Within actix-web, middleware is used as a medium for being able to add general functionality to a (set of) route(s) by taking the request before the handler function runs, carrying out some operations, running the actual handler function itself and then the middleware does additional processing (if required). By default, actix-web has several default middlewares that we can use, including logging, path normalisation, access external services and modifying application state (through the ServiceRequest type).

See below for an example of how to implement a default Logger middleware:

use actix_web::{middleware::Logger, App};

#[actix_web::main]
async fn main() -> std::io::Result<()> {
// access logs are printed with the INFO level so ensure it is enabled by default
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));

let app = App::new()
    .wrap(Logger::default());
    // ... rest of your code
}
Enter fullscreen mode Exit fullscreen mode

Additionally, you can also write your own middleware in Actix Web! For simple use cases, we can use an FnOnce closure to be able to write whatever we want (for example, printing a message like in this instance:

let app = App::new()
    .wrap_fn(|req, srv| {
        println!("Hi from start. You requested: {}", req.path());
        srv.call(req).map(|res| {
            println!("Hi from response");
            res
        })
    })
    .route(
        "/",
        web::get().to(|| async { "Hello, middleware!" }),
);
Enter fullscreen mode Exit fullscreen mode

To be able to write more complex middleware, we actually need to implement two traits - Service<Req> which is for implementing the actual middleware itself as well as Transform<S, Req> which is the required for the builder for the actual Service that handles requests (in terms of when we're building our service, we will actually use the builder, not the middleware! The outer service will automatically call the middleware upon detecting a HTTP request).

For an actual middleware implementation, let's have a look at writing a middleware that simply prints messages. We can create the builder for the middleware by implementing the Transform trait:

use std::{future::{ready, Ready, Future}, pin::Pin};

use actix_web::{
    dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
    web, Error,
};

pub struct SayHi;

// `S` - type of the next service
// `B` - type of response's body
impl<S, B> Transform<S, ServiceRequest> for SayHi
where
    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
    S::Future: 'static,
    B: 'static,
{
    // setting up the types for the middleware to work
    type Response = ServiceResponse<B>;
    type Error = Error;
    type InitError = ();
    type Transform = SayHiMiddleware<S>;
    type Future = Ready<Result<Self::Transform, Self::InitError>>;

    // this immediately returns the middleware
    fn new_transform(&self, service: S) -> Self::Future {
        ready(Ok(SayHiMiddleware { service }))
    }
}
Enter fullscreen mode Exit fullscreen mode

Now we can write the middleware itself! Internally the middleware must implement a generic type - which then gets declared in the Service trait. Note that we manually re-implement a type from futures_util called LocalBoxFuture - that is to say, a future that doesn't require the Send trait and is safe to use because it implements Unpin on dereferencing, which automatically cancels any previous thread-safety guarantees.

pub struct SayHiMiddleware<S> {
    /// The next service to call
    service: S,
}

// This future doesn't have the requirement of being `Send`.
// See: futures_util::future::LocalBoxFuture
type LocalBoxFuture<T> = Pin<Box<dyn Future<Output = T> + 'static>>;

// `S`: type of the wrapped service
// `B`: type of the body - try to be generic over the body where possible
impl<S, B> Service<ServiceRequest> for SayHiMiddleware<S>
where
    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
    S::Future: 'static,
    B: 'static,
{
    type Response = ServiceResponse<B>;
    type Error = Error;
    type Future = LocalBoxFuture<Result<Self::Response, Self::Error>>;

    // This service is ready when its next service is ready
    forward_ready!(service);

    fn call(&self, req: ServiceRequest) -> Self::Future {
        println!("Hi from start. You requested: {}", req.path());

        // A more complex middleware, could return an error or an early response here.

        // we do not immediately await this, which means nothing happens
        // this future gets moved into a Box
        let fut = self.service.call(req);

        Box::pin(async move {
            // this future gets awaited now
            let res = fut.await?;

            // we can now do any work we need to after the request
            println!("Hi from response");
            Ok(res)
        })
    }
}
Enter fullscreen mode Exit fullscreen mode

Now that we've written our middleware, we can now add it to our App:

#[actix_web::main]
async fn main() -> std::io::Result<()> {
// access logs are printed with the INFO level so ensure it is enabled by default
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));

let app = App::new()
     .wrap(SayHi);
     // ... rest of your code
}
Enter fullscreen mode Exit fullscreen mode

Static Files in Actix Web

Plain, no-frills static file serving in actix-web is done through the actix_files crate - to add it, you simply need to add it through Cargo like so:

cargo add actix-files 
Enter fullscreen mode Exit fullscreen mode

Setting up a route for static file serving would look like this:

use actix_files::NamedFile;
use actix_web::{HttpRequest, Result};
use std::path::PathBuf;

#[get("/")]
async fn index(req: HttpRequest) -> Result<NamedFile> {
    let path: PathBuf = req.match_info().query("filename").parse().unwrap();
    Ok(NamedFile::open(path)?)
}
Enter fullscreen mode Exit fullscreen mode

This route allows us to serve any file that can be found and matches the filename - for example, if we have a base route that serves this route, if we then run our app and go to /index.html, the route will try to look for a file named index.html in the project root.

Note that you should not under any circumstances try and use a path tail with .* to return a NamedFile - this has serious security implications and will open your web service up to path traversal! This is documented in the actix-web docs here, and you can find more about path traversal attacks here. As a guard against this, you can attempt to validate the path file is correct or is not attempting to traverse outside of the intended folder by using std::fs::canoncalize).

However, this is a bit clumsy when you need to serve multiple files - particularly if you need to serve a folder of HTML files, for instance. To be able to serve a folder of files from your web service, the best way to do this would be to use actix_files::Files and attach it to your App:

use actix_files as fs;
use actix_web::{App, HttpServer};

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new().service(
            fs::Files::new("/static", ".")
                .use_last_modified(true),
        )
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}
Enter fullscreen mode Exit fullscreen mode

Note that you can also augment the Files struct with several options like showing the file directory itself at the base route (for the file service) and allowing hidden files, which you can find more about here.

Additionally, we can also use the power of HTML templating with askama to supercharge our HTML file serving! We can get started like so:

cargo add askama askama-actix-web -F askama/with-actix-web
Enter fullscreen mode Exit fullscreen mode

This adds askama itself as well as the Responder trait implementation for the askama::Template type. askama expects your files to be in a subfolder of the project root called templates by default, so let's create the folder and then create an index.html file with the following HTML code in:

Hello, {{name}}!
Enter fullscreen mode Exit fullscreen mode

To use Askama in our app, we need to declare a struct that uses the Template derive macro and use askama's template macro to point the struct to the file that we want it to use:

#[derive(Template)]
#[template(path = "index.html")]
struct IndexTemplate<'a> {
    name: &'a str
}

#[get("/")]
async fn index_route() -> impl Responder {
    IndexTemplate { name: "Shuttle" }
}
Enter fullscreen mode Exit fullscreen mode

Then we can add it as a regular handler function in our actix-web service and we're good to go! When you go to a path that returns index_route, you should see "Hello, Shuttle!" as the HTML response.

Interested in learning more about Askama? We have a Shuttle Launchpad newsletter issue that talks about this! You can find it here.

Deploying

Deployment with Rust backend programs, in general, can be less than ideal due to having to use Dockerfiles, although if you are experienced with Docker already this may not be such an issue for you - particularly if you are using cargo-chef. However, if you're using Shuttle you can just use cargo shuttle deploy and you're done already. No setup is required.

Finishing Up

Thanks for reading! The actix-web Rust crate is a strong framework that you can use to boost your Rust portfolio and is a great framework to do a deep dive into Rust with if you're looking to build your first Rust API.

Interested in more?

💖 💪 🙅 🚩
shuttle_dev
Shuttle

Posted on December 15, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related