Кратко про библиотеку Axum в Rust

014397c6f75361dbc9bcdbfbf8bbd35d.png

Привет, Хабр!

Axum была создана командой Tokio, которая уже получила свой +rep за создание асинхронной платформы Tokio для Rust.

Axum является микрофреймворком, ориентированным на упрощение задач, связанных с маршрутизацией и обработкой запросов в веб-приложениях.

Основная фича Axum заключается в его возможности обрабатывать тысячи запросов без значительных затрат на ресурсы.

Маршрутизация

Axum использует декларативный подход к маршрутизации. Он достигается с помощью макросов и функций, которые позволяют понятно описать структуру веб-сервиса. Например, базовая настройка маршрута в Axum:

use axum::{Router, routing::get};

async fn root_handler() -> &'static str {
    "Hello, Axum!"
}

#[tokio::main]
async fn main() {
    let app = Router::new().route("/", get(root_handler));
    axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

Создали базовый маршрут / который обрабатывается функцией root_handler. Здесь используется HTTP метод GET.

Axum поддерживает все стандартные HTTP методы, такие как GET, POST, PUT, DELETE, и т.д. Это поможет создать RESTful API с четким разделением ответственности между различными операциями. Пример маршрутизации с несколькими HTTP методами:

use axum::{Router, routing::{get, post, put, delete}};

async fn get_handler() -> &'static str {
    "GET request"
}

async fn post_handler() -> &'static str {
    "POST request"
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/resource", get(get_handler).post(post_handler));
    // ...
}

/resource маршрут принимает как GET, так и POST запросы, каждый из которых обрабатывается соответствующей функцией.

Можно интегрировать обработку ошибок в систему маршрутизации. Например, можно определить маршрут, который обрабатывает ошибки HTTP и возвращает пользовательские сообщения об ошибках:

use axum::{Router, routing::get, http::StatusCode, response::Response, Error};

async fn handle_error(error: Error) -> (StatusCode, String) {
    (StatusCode::INTERNAL_SERVER_ERROR, format!("Something went wrong: {}", error))
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/", get(root_handler))
        .handle_error(handle_error);
    //  ...
}

Динамические URL параметры в Axum позволяют передавать значения через URL. Например, можно создать маршрут, который извлекает ID пользователя из URL:

use axum::{Router, routing::get, extract::Path};

async fn user_handler(Path(user_id): Path) -> String {
    format!("User ID: {}", user_id)
}

#[tokio::main]
async fn main() {
    let app = Router::new().route("/user/:user_id", get(user_handler));
    // настройка и запуск сервера...
}

Маршрут /user/:user_id динамически извлекает user_id из URL и передает его в обработчик user_handler.

Extractors и Middleware

В Axum Extractors автоматически обрабатывают данные запроса и преобразуют их в нужные типы перед передачей в handler функции. Например, можно извлекать параметры пути, строки запроса или JSON тела запроса напрямую в параметры функции обработчика:

use axum::{extract::{Path, Query, Json}, Router, routing::get};
use serde_json::Value;
use std::collections::HashMap;

async fn handle_request(
    Path(user_id): Path, 
    Query(params): Query>, 
    Json(payload): Json
) -> String {
    format!("User ID: {}, Params: {:?}, Payload: {}", user_id, params, payload)
}

А вот Middleware уже используется для обработки запросов до или после того, как они достигнут вашего обработчика. Так можно реализовать аутентификацию, логирование, сжатие ответов и множество других задач, которые должны выполняться централизованно. Middleware в Axum реализуется с использованием библиотеки Tower, которая позволяет удобно строить сложные цепочки обработки запросов.

Пример middleware для аутентификации:

use axum::{Router, routing::get, middleware::from_fn, http::Request};
use tower::ServiceBuilder;

async fn auth_middleware(req: Request, next: Next) -> Result {
    if authorized(&req) {
        next.run(req).await
    } else {
        Ok(Response::builder().status(StatusCode::UNAUTHORIZED).body("Unauthorized".into()).unwrap())
    }
}

let app = Router::new()
    .route("/", get(handler))
    .layer(ServiceBuilder::new().layer(from_fn(auth_middleware)));

Все запросы проходят через функцию auth_middleware, которая проверяет, авторизован ли юзер, прежде чем обработать запрос.

Также можно создать пользовательский Extractor! Для этого необходимо реализовать трейт FromRequest или FromRequestParts для необходимого типа. Эти трейты позволяют извлекать данные из частей запроса или из всего запроса соответственно.

Например:

use axum::{
    async_trait,
    extract::{FromRequest, RequestParts},
    Request,
};
use std::convert::Infallible;

struct MyExtractor {
    value: String,
}

#[async_trait]
impl FromRequest for MyExtractor
where
    S: Send,
{
    type Rejection = Infallible;

    async fn from_request(req: Request) -> Result {
        let value = String::from("extracted value"); // логика извлечения
        Ok(MyExtractor { value })
    }
}

MyExtractor извлекает данные из запроса, превращая их в структуру с полем value.

После создания extractor, его можно использовать в обработчиках запросов для получения данных:

async fn handle_request(custom: MyExtractor) -> String {
    format!("Extracted value: {}", custom.value)
}

Можно также создать условные extractor, которые могут принимать или отклонять запросы на основе содержимого запроса. Например, можно обернуть extractor в Option или Result, чтобы обрабатывать возможные ошибки извлечения данных:

async fn handle_optional_data(data: Option) -> String {
    match data {
        Some(extractor) => format!("Received data: {}", extractor.value),
        None => "No data received".to_string(),
    }
}

Интеграция с БД

Сложно представить хороший веб-фреймворк без работы с БД.

Интеграция БД в Axum можно провести с SQLx. Все начинается с настройки зависимостей в Cargo.toml. Пример конфигурации для работы с PostgreSQL:

[dependencies]
sqlx = { version = "0.5.5", features = ["postgres"] }
tokio = { version = "1.0", features = ["full"] }

Для работы с БД создается пул соединений, который затем может быть интегрирован в приложение Axum. Пример создания пула соединений и его передачи в обработчик запросов:

use axum::{Router, routing::get, Extension};
use sqlx::postgres::PgPool;
use std::net::SocketAddr;

async fn get_users(Extension(pool): Extension) -> String {
    // пример запроса к базе данных
    let users = sqlx::query!("SELECT username FROM users")
        .fetch_all(&pool)
        .await
        .unwrap();

    // обработка и возврат результатов
    format!("Found users: {:?}", users)
}

#[tokio::main]
async fn main() {
    let pool = PgPool::connect("postgres://user:password@localhost/database").await.unwrap();
    let app = Router::new().route("/users", get(get_users)).layer(Extension(pool));

    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    axum::Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

Реализуем REST API с Axum

Для начала создадим проект Rust:

cargo new axum_rest_api
cd axum_rest_api

Добавим зависимости в файл Cargo.toml:

[dependencies]
axum = "0.6"
tokio = { version = "1.0", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
sqlx = { version = "0.6", features = ["postgres"] }
dotenv = "0.15"

В src/main.rs настроим базовую асинхронную среду:

use axum::{
    routing::get,
    Router,
};
use std::net::SocketAddr;

#[tokio::main]
async fn main() {
    let app = Router::new().route("/", get(root_handler));

    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    println!("Listening on {}", addr);
    axum::Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

async fn root_handler() -> &'static str {
    "Hello, HABR!"
}

Добавим поддержку dotenv для управления переменными окружения:

dotenv::dotenv().ok();
let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");

Юзаем sqlx для подключения к базе данных:

let db_pool = sqlx::PgPool::connect(&database_url).await.unwrap();

Определим маршруты для CRUD-операций:

let app = Router::new()
    .route("/", get(root_handler))
    .route("/users", get(get_users).post(add_user))
    .route("/users/:id", get(get_user).put(update_user).delete(delete_user))
    .layer(Extension(db_pool));

Обработчики для каждого маршрута:

async fn get_users(Extension(db_pool): Extension) -> impl IntoResponse {
    match sqlx::query_as!(User, "SELECT * FROM users")
        .fetch_all(&db_pool)
        .await
    {
        Ok(users) => (StatusCode::OK, Json(users)),
        Err(_) => (
            StatusCode::INTERNAL_SERVER_ERROR,
            Json(json!({"error": "Failed to fetch users"}))
        ),
    }
} // обработка результатов 

async fn add_user(Json(payload): Json, Extension(db_pool): Extension) -> impl IntoResponse {
    let query = "INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id, name, email";
    match sqlx::query_as!(User, query, payload.name, payload.email)
        .fetch_one(&db_pool)
        .await
    {
        Ok(user) => (StatusCode::CREATED, Json(user)),
        Err(_) => (
            StatusCode::INTERNAL_SERVER_ERROR,
            Json(json!({"error": "Failed to add user"}))
        ),
    }
} // добавление юзеров

Для работы с БД юзаем SQLx, который поддерживает асинхронные запросы и предоставляет безопасные по типам запросы:

let user = sqlx::query_as!(User, "INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id, name, email", &payload.name, &payload.email)
    .fetch_one(&db_pool)
    .await
    .unwrap();

Примерно так будет выглядеть базовое REST API на Axum в Rust.

Как вам библиотека? Знаете аналоги получше?

Напомню, что про другие интересные и полезные библиотеки мои коллеги из OTUS рассказывают в рамках практических онлайн-курсов. С каталогом курсов можно ознакомиться по ссылке.

© Habrahabr.ru