Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Роутинг

В этой главе мы подробнее разберёмся с возможностями роутинга эндпоинтов.

Фоллбэк (fallback)

Если путь из введённого пользователем URL не соответствует ни одному эндпоинту, зарегистрированному в роутере, то Axum по умолчанию просто ответит HTTP кодом 404.

Если вам необходимо обрабатывать такие запросы к несуществующим эндпоинтам “вручную”, то можно задать фоллбэк (fallback) обработчик. Фоллбэк — это обычная функция-обработчик запросов, которая регистрируется в роутере при помощи метода fallback и вызывается для всех запросов, для которых не был найден соответствующий эндпоинт.

В качестве примера сделаем фоллбэк обработчик, который просто отображает HTTP метод и URL запроса.

use axum::{Router, http::request::Parts, routing::get};

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/hello", get(hello))
        .fallback(my_fallback);
    let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

async fn hello() -> &'static str {
    "Hello!"
}

async fn my_fallback(parts: Parts) -> String {
    format!("Method: {}\nURL: {}", parts.method, parts.uri)
}

После запуска программы, если перейти на http://localhost:8080/hello, то мы увидим “Hello!”, но попытка указать любой другой путь, например, http://localhost:8080/non-existing-page?a=1&b=2 приведёт к ответу вида:

Method: GET
URL: /non-existing-page?a=1&b=2

Слияние роутеров (merging)

Роутер эндпоинтов можно собирать из других под-роутеров. То есть мы можем определить одну часть эндпоинтов в одном объекте роутера, другую — в другом и потом “склеить” эти роутеры в главный роутер методом merge.

Для примера создадим один роутер для эндпоинтов, связанных с данными пользователей, а другой роутер для эндпоинтов, связанных с товарами.

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

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

    let products_router = Router::new()
        .route("/products", get(list_products));

    let app = Router::new()
        .merge(users_router)
        .merge(products_router);
    let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

async fn list_users() -> &'static str {
    "Users"
}

async fn list_products() -> &'static str {
    "Products"
}

Такое разнесение эндпоинтов по отдельным роутерам позволяет писать более модульный код. Но читабельность кода — не единственное преимущество, которое мы получаем, собирая роутер из под-роутеров. Этот подход так же позволяет задавать отдельные объекты состояния (State) для каждого из роутеров, обеспечивая тем самым изоляцию доступа к частям состояния приложения.

Модифицируем наш пример так, что для каждого из под-роутеров будет использоваться свой объект состояния.

use std::sync::Arc;

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

#[derive(Debug)]
struct UserState {}

#[derive(Debug)]
struct ProductState {}

#[tokio::main]
async fn main() {
    let users_router = Router::new()
        .route("/users", get(list_users))
        .with_state(Arc::new(UserState {}));

    let products_router = Router::new()
        .route("/products", get(list_products))
        .with_state(Arc::new(ProductState {}));

    let app = Router::new().merge(users_router).merge(products_router);
    let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

async fn list_users(State(state): State<Arc<UserState>>) -> String {
    format!("Users endpoint. State: {state:?}")
}

async fn list_products(State(state): State<Arc<ProductState>>) -> String {
    format!("Products endpoint. State: {state:?}")
}

Если мы запустим приложение и перейдём по адресу http://localhost:8080/users, то мы увидим “Users endpoint. State: UserState”.

Если перейти на http://localhost:8080/products, то должно отобразиться — “Products endpoint. State: ProductState”.

Как видим, эндпоинты работают с тем объектом состояния, который был задан в их под-роутере. Но что случится, если мы добавим еще один объект состояния на уровень объединяющего роутера?

#[derive(Debug)]
struct MainState {}

let app = Router::new()
    .merge(users_router)
    .merge(products_router)
    .with_state(Arc::new(MainState));

Не изменится ничего: обработчики используют тот объект состояния, который объявлен “ближе всего” к ним.

Однако если мы удалим объект состояния, объявленный на уровне users_router, и добавим объект состояния типа UserState на уровне главного роутера, тогда при обращении к http://localhost:8080/users будет использоваться объект состояния из главного роутера.

Встраивание под-роутеров (nesting)

Встраивание (nesting) подобно вышерассмотренному слиянию (merging) с тем отличием, что при встраивании пути эндпоинтов из встраиваемого роутера предваряются указанным префиксом.

Например, имеется роутер:

let users_router = Router::new().route("/users", get(list_users));

Если его встроить в главный роутер так:

let main_router = Router::new().nest("/api", users_router);

то функция-обработчик list_users будет срабатывать для запроса с URL http://localhost:8080/api/users.

Для чего это нужно? Это позволяет еще больше повысить модульность кода. Например, мы можем вынести некоторые эндпоинты в отдельную библиотеку и встроить их в роутер в другом приложении, не опасаясь получить конфликт путей с другими эндпоинтами.

Приведём пример. Создадим два роутера, содержащие эндпоинты с конфликтующими путями. Используя разные префиксы, встроим эти два роутера в главный роутер.

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

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

    let users_v2_router = Router::new().route("/users", get(list_users_v2));

    let app = Router::new()
        .nest("/api/v1", users_v1_router)
        .nest("/api/v2", users_v2_router);
    let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

async fn list_users_v1() -> &'static str {
    "Users endpoint Version 1"
}

async fn list_users_v2() -> &'static str {
    "Users endpoint Version 2"
}

При переходе на http://localhost:8080/api/v1/users отображается “Users endpoint Version 1”, а при переходе на http://localhost:8080/api/v2/users — “Users endpoint Version 2”.