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)
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!