Getting Started with Actix Web

Hi, today I will try to explain everything I wish I knew when I started developing on Actix Web.

A little bit of poetry to begin with.

Rust – a general-purpose, multi-paradigm compiled programming language developed by Mozilla. I highly recommend learning the basic concepts, types, syntax of the language, learn a little about cargo.

Actix Web – a high-performance web framework for Rust. This is what the article is about.

This article describes how to write basic functions, use app_state, json, path in requests. It also shows how to create middleware

Preparation.

1. Installing Rust (If for some reason it is not there)

  1. Initializing the project and installing dependencies

cargo init --bin actix_test # Инициализация проекта
cd actix_test

Let's add the necessary dependencies

cargo add actix-web env_logger log \
  chrono --features chrono/serde \
  serde --features serde/derive serde_json \ 

Let's run through the dependencies a little.

Env_logger And log – logging in the application

Chrono – library for working with time

Serde – serialization and deserialization from different data types. In our case serde_json

Let's start writing code.

// main.rs
// Базовая структура проекта на actix web

// Импорты
use actix_web::{App, HttpServer};
use actix_web::middleware::Logger;
use log::info;

#[actix_web::main] // Макрос для адекватной работы async fn main() 
async fn main() -> std::io::Result<()> {
    // Для работы библиотеки log
    env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));

    // Просто чтобы понимать, что сервер запущен. Полезно в контейнерах
    info!("Successfully started server");

    // Грубо говоря конфигурация HttpServer
    HttpServer::new(|| {
        // Собственно само приложение со всеми handlers, middleware,
        // информации приложения и т.д
        App::new()
            // .wrap() позволяет добавить middleware (промежуточную функцию) приложению
            .wrap(Logger::default())
    }).bind("0.0.0.0:8080")
        .unwrap()
        .run()
        .await
}

After compiling and running the project, you can send any request to localhost:8080 and the answer will always be 404.

Let's fix this by writing a simple handler that will return Hello!

// main.rs
// Нужные импорты, не надо изменять предыдущие.
use actix_web::{get, Responder};

#[get("/")] // указывается тип запроса("/путь"),
// У этого handler запрос будет на http://localhost:8080
async fn hello() -> impl Responder // Responder это trait, который позволяет
// преобразовывать тип данных в HttpResponse. В основном он используется для 
// примитивных функций
{ 
  "Hello!"
}

// Добавим handler в App

#[actix_web::main] // Макрос для адекватной работы async fn main() 
async fn main() -> std::io::Result<()> {
    // Прежний код
    HttpServer::new(|| {
        App::new()
            .wrap(Logger::default())
            // .service() необходим для создания handlers
            .service(hello)
    }) // Прежний код
}

After compilation when visiting localhost:8080 there will be an HttpResponse with code 200 and the text we wrote.

Application status

Let's create a struct in main.rs..

// main.rs

use actix_web::web::Data;
use std::sync::Mutex;

// Прежний код

// В этом struct нужно прописывать все, что может понадобиться
pub(crate) struct AppState {
    app_name: String,
    req_counter: Mutex<u32>
}

// Если переменная должна быть мутабельной, то надо использовать 
// name: Mutex<T>
// После вызывая app_state.name.lock().unwrap() для изменений

#[actix_web::main] 
async fn main() -> std::io::Result<()> {
    // Прежний код

    let app_state = Data::new(AppState {
        app_name: "test".to_string(),
        req_counter: Mutex::new(0)
    });
    
    info!("Successfully started server");
    
    HttpServer::new(move || { // Необходимо добавить move
        App::new()
            .wrap(Logger::default())
            .app_data(app_state.clone())
            .service(hello)
    }) // Прежний код
}

Let's make a separate file app_state.rs and link it to main.rs

// main.rs

mod app_state;

// app_state.rs

use actix_web::{get, Responder};
use actix_web::web::Data;
use crate::AppState;

#[get("/app_name")]
pub(crate) async fn app_name(app_state: Data<AppState>) -> impl Responder {
    // Возвращаем имя из app_state
    app_state.app_name.clone()
}

#[get("/req")]
pub(crate) async fn req_counter(app_state: Data<AppState>) -> impl Responder {
    let mut req_counter = app_state.req_counter.lock().unwrap();
    *req_counter += 1;
    format!("Requests sent: {}", req_counter)
}

Let's add a handler

//main.rs

use app_state::{app_name, req_counter};

#[actix_web::main] 
async fn main() -> std::io::Result<()> {
    // Прежний код
    HttpServer::new(move || { 
        App::new()
            .wrap(Logger::default())
            .app_data(app_state.clone())
            .service(hello)
            .service(app_name)
            .service(req_counter)
    }) // Прежний код
}

We launch and send requests to localhost:8080/app_name And localhost:8080/req

JSON

Let's make a separate file json.rs and link it to main.rs

// main.rs

mod json;

// json.rs

use actix_web::{HttpResponse, post};

#[post("/register")]
pub(crate) async fn json_test() -> HttpResponse 
// HttpResponse это ответ сервера, который содержит статус код и информацию ответа
{ 
    // Пустой Ok (200) ответ
    HttpResponse::Ok().finish()
}

actix_web has a special type for json. It accepts the type

// json.rs

use actix_web::{HttpResponse, post};
use actix_web::web::Json;
use serde::Deserialize;

// Подробнее про derive можно почитать тут 
// https://doc.rust-lang.org/reference/procedural-macros.html#derive-macros
// TL;DR генерация кода 
#[derive(Deserialize, Debug)]
struct Test{
    field1: String,
    field2: u32
}

#[post("/json/test")]
pub(crate) async fn json_test(json: Json<Test>) -> HttpResponse {
    println!("{:?}", json);
    HttpResponse::Ok().finish()
}

Now let's return the information in Json format

use actix_web::get;

#[get("/json/time")]
pub(crate) async fn json_time() -> HttpResponse {
    let current_utc = chrono::Utc::now();

    HttpResponse::Ok().json(current_utc)
}

Let's add handlers

// main.rs

use json::{json_test, json_time};

// Прежний код
#[actix_web::main] 
async fn main() -> std::io::Result<()> {
    // Прежний код
    HttpServer::new(move || { 
        App::new()
            .wrap(Logger::default())
            .app_data(app_state.clone())
            .service(hello)
            .service(app_name)
            .service(req_counter)
            .service(json_test)
            .service(json_time)
    }) // Прежний код
}

Compile, run, test.

We will send a post request to localhost:8080/json/test with such a payload

{ “field1”: “String”, “field2”: 123 }

You can see the result in the console

Json(Test { field1: “String”, field2: 123 })

Let's send a get request to localhost:8080/json/time and get the current UTC time

Paths in url

Let's create path.rs

// main.rs
mod path;

// path.rs

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

// Для web::Path можно указывать другие типы данных, например u32
#[get("/{path}")]
pub(crate) async fn single_path(path: web::Path<String>) -> HttpResponse {
    HttpResponse::Ok().body(format!("You looked for {}", path))
}

#[get("/{path1}/{path2}")]
pub(crate) async fn multiple_paths(path: web::Path<(String, String)>) -> HttpResponse {
    let (path1, path2) = path.into_inner();

    HttpResponse::Ok().body(format!("You looked for {}/{}", path1, path2))
}

Let's add handlers

// main.rs

use path::{single_path, multiple_paths};

// Прежний код
#[actix_web::main] 
async fn main() -> std::io::Result<()> {
    // Прежний код
    HttpServer::new(move || { 
        App::new()
            .wrap(Logger::default())
            .app_data(app_state.clone())
            .service(hello)
            .service(app_name)
            .service(req_counter)
            .service(json_test)
            .service(json_time)
            .service(single_path)
            .service(multiple_paths)
    }) // Прежний код
}

Some tests
localhost:8080/path And localhost:8080/path1/path2

Middleware

I find their spelling strange.

To write middleware, let's add another library

cargo add futures-util 

Let's create a middleware.rs file

// main.rs
mod middleware;

// middleware.rs

use std::future::{Ready, ready};
use actix_web::body::EitherBody;
use actix_web::dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform};
use actix_web::{Error, HttpResponse};
use futures_util::future::LocalBoxFuture;
use futures_util::FutureExt;

// Имя middleware
pub struct Test;

// Если интересно, то можно почитать тут
// https://docs.rs/actix-service/latest/actix_service/trait.Transform.html
// Если нет, то смотри ниже
impl<S, B> Transform<S, ServiceRequest> for Test
where
    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
    S::Future: 'static,
    B: 'static,
{
    type Response = ServiceResponse<EitherBody<B>>;
    type Error = Error;
    type InitError = ();
    type Transform = TestMiddleware<S>;
    type Future = Ready<Result<Self::Transform, Self::InitError>>;

    fn new_transform(&self, service: S) -> Self::Future {
        ready(Ok(TestMiddleware { service }))
    }
}

// Рекомендуется использовать имя Middleware + слово Middleware
pub struct TestMiddleware<S> {
    service: S,
}

// https://docs.rs/actix-service/latest/actix_service/trait.Service.html
impl<S, B> Service<ServiceRequest> for TestMiddleware<S>
where
    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
    S::Future: 'static,
    B: 'static,
{
    type Response = ServiceResponse<EitherBody<B>>;
    type Error = Error;
    type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;

    forward_ready!(service);

    fn call(&self, req: ServiceRequest) -> Self::Future {
        // В целом тут прописывается вся логика

        if req.headers().contains_key("random-header-key") {
            // Логика ошибки
            let http_res = HttpResponse::BadRequest().body("Not allowed to have \'random-header-key\' header");
            let (http_req, _) = req.into_parts();
            let res = ServiceResponse::new(http_req, http_res);
            return (async move { Ok(res.map_into_right_body()) }).boxed_local();
        }

        println!("{}", req.method());

        let fut = self.service.call(req);

        Box::pin(async move {
            let res = fut.await?;
            Ok(res.map_into_left_body())
        })
    }
}

It looks scary, but everything is probably fine. By the way, here is the documentation for middleware

Let's add middleware

// main.rs

use middleware::Test;

// Прежний код
#[actix_web::main] 
async fn main() -> std::io::Result<()> {
    // Прежний код
    HttpServer::new(move || { 
        App::new()
            // Middleware идут по очереди по обратному порядку определения
            // Тоесть сначала отработет Test и потом Logger
            .wrap(Logger::default())
            .wrap(Test)
            .app_data(app_state.clone())
            .service(hello)
            .service(app_name)
            .service(req_counter)
            .service(json_test)
            .service(json_time)
            .service(single_path)
            .service(multiple_paths)
    }) // Прежний код
}

Useful links

Documentation actix_web

Docs.rs
Examples

Conclusion

Actix-web is a powerful tool. In this article I have shown what I think is a convenient way to use it. But there are several other ways to do what I have shown.

It is important to understand why and when actix-web is needed (and Rust in general).
Use it if you need gigantic performance that other languages ​​can't offer, otherwise don't.

Thanks for reading, good luck in mastering something new!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *