Sadržaj članka
  1. Uvod u Axum i Rust za Backend Razvoj
  2. Postavljanje Okoline i Projekta
  3. Inicijalizacija Projekta
  4. Osnovna “Hello World” Aplikacija
  5. Definiranje Modela Podataka s Serde
  6. Implementacija CRUD Operacija (Stvaranje, Čitanje, Ažuriranje, Brisanje)
  7. Upravljanje Stanstvom Aplikacije (Shared State)
  8. Objašnjenje ključnih Axum koncepata:
  9. Testiranje API-ja
  10. Napredniji Koncepti i Dobre Prakse
  11. Error Handling
  12. Middleware
  13. Modularizacija Ruta
  14. Zaključak

Uvod u Axum i Rust za Backend Razvoj

U današnjem svijetu, brzina, sigurnost i pouzdanost ključni su faktori za uspješne web aplikacije. Rust programski jezik sve se više prepoznaje kao iznimno moćan alat za razvoj backend sustava koji udovoljavaju ovim zahtjevima. Njegova bezkompromisna sigurnost memorije (bez garbage collectora), konkurentnost bez utrka podataka i izvanredne performanse čine ga idealnim izborom za izgradnju visokoopterećenih API-ja. Međutim, Rust sam po sebi je niskorazinski, što zahtijeva postojanje visokokvalitetnih frameworka koji olakšavaju razvoj. Tu na scenu stupa Axum, moderni web framework izgrađen na temeljima popularnog Tokio asinkronog runtimea i Hyper HTTP biblioteke. Axum se ističe svojom modularnošću, fleksibilnošću i snažnom podrškom za tipove (zahvaljujući Rustovom sustavu tipova), što rezultira manjim brojem grešaka i lakšim održavanjem koda.

Ovaj vodič ima za cilj pružiti praktičan uvid u izgradnju RESTful API-ja koristeći Axum. Proći ćemo kroz ključne koncepte, od postavljanja projekta do implementacije ruta, rukovatelja i obrade podataka, s naglaskom na asinkroni I/O i idiome Rusta.

Postavljanje Okoline i Projekta

Prije nego što zaronimo u kod, osigurajmo da imamo sve potrebne alate instalirane i konfigurirane. Pretpostavlja se da već imate instaliran Rust toolchain (preko rustup).

Inicijalizacija Projekta

Započnimo stvaranjem novog Cargo projekta:

cargo new my_axum_api --bin
cd my_axum_api

Zatim, u Cargo.toml datoteku dodajte potrebne ovisnosti. Za naš API, trebat će nam axum, tokio (s full značajkama za asinkroni runtime), serde (za serijalizaciju/deserijalizaciju JSON-a) i serde_json.

# Cargo.toml

[package]
name = "my_axum_api"
version = "0.1.0"
edition = "2021"

[dependencies]
axum = "0.7"
tokio = { version = "1.36", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

axum je glavni web framework. tokio je asinkroni runtime koji Axum koristi. serde je biblioteka za serijalizaciju i deserijalizaciju Rust struktura u različite formate, dok je serde_json specifična implementacija za JSON. features = ["derive"] za serde omogućava automatsko generiranje koda za serijalizaciju/deserijalizaciju putem makroa #[derive(Serialize, Deserialize)].

Osnovna “Hello World” Aplikacija

Prije nego što krenemo s kompleksnijim stvarima, postavimo jednostavan Axum server koji će vratiti "Hello, World!" poruku.

Otvorite src/main.rs i dodajte sljedeći kod:

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

#[tokio::main]
async fn main() {
    // Kreiranje Axum routera.
    // `get` metoda definira HTTP GET putanju `/` i povezuje je s `handler` funkcijom.
    let app = Router::new().route("/", get(handler));

    // Definiranje adrese na kojoj će server slušati.
    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    println!("Server sluša na {}", addr);

    // Pokretanje servera s Tokio runtimeom.
    axum::Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

// Asinkrona funkcija koja će obrađivati zahtjeve na putanji `/`
async fn handler() -> String {
    "Hello, World from Axum!".to_string()
}

Pokrenite aplikaciju:

cargo run

Otvorite preglednik ili koristite curl i posjetite http://127.0.0.1:3000. Trebali biste vidjeti poruku "Hello, World from Axum!".

  • #[tokio::main] makro je esencijalan. On pretvara main funkciju u asinkronu i inicijalizira Tokio runtime, omogućujući nam korištenje await ključne riječi.
  • Router::new() stvara instancu routera.
  • .route("/", get(handler)) definira rutu za GET zahtjeve na putanji / i povezuje je s funkcijom handler.
  • axum::Server::bind(&addr).serve(app.into_make_service()).await.unwrap(); pokreće HTTP server.

Definiranje Modela Podataka s Serde

Za RESTful API-je, često radimo s JSON podacima. serde i serde_json su standardni Rust alati za to. Definirajmo jednostavnu strukturu za predstavljanje korisnika.

use serde::{Deserialize, Serialize};

// #[derive(Serialize, Deserialize)] makroi automatski generiraju kod za
// serijalizaciju (u JSON) i deserijalizaciju (iz JSON-a).
#[derive(Debug, Serialize, Deserialize, Clone)]
struct User {
    id: u32,
    name: String,
    email: String,
}
  • #[derive(Debug, Serialize, Deserialize, Clone)]: Debug je koristan za ispis u konzolu, Serialize i Deserialize su ključni za JSON, a Clone je često potreban za kopiranje podataka u različitim kontekstima unutar asinkronih rukovatelja.

Implementacija CRUD Operacija (Stvaranje, Čitanje, Ažuriranje, Brisanje)

Sada ćemo implementirati osnovne CRUD operacije za našeg User modela. Za jednostavnost, koristit ćemo Vec<User> kao in-memory "bazu podataka". U produkciji biste ovdje koristili pravu bazu podataka (PostgreSQL, MongoDB, itd.) i sqlx, diesel ili neki drugi ORM/ODM.

Upravljanje Stanstvom Aplikacije (Shared State)

Kako bismo omogućili da svi rukovatelji pristupaju istoj "bazi podataka", trebat će nam shared state. U Axumu se to obično radi s Arc (Atomic Reference Counted) i Mutex (ili RwLock za finiju kontrolu). Arc omogućava višestruko vlasništvo nad podacima, a Mutex osigurava siguran pristup podacima iz višestrukih konkurentnih threadova/taskova.

Prvo, dodajmo tokio::sync::Mutex u Cargo.toml:

# Cargo.toml

[dependencies]
...
tokio = { version = "1.36", features = ["full", "sync"] } # Dodana značajka 'sync'

Sada modificirajmo main.rs za inicijalizaciju shared state-a:

use axum::{
    extract::{Path, State},
    http::StatusCode,
    response::IntoResponse,
    routing::{get, post, put, delete},
    Json,
    Router,
};
use serde::{Deserialize, Serialize};
use std::{net::SocketAddr, sync::Arc};
use tokio::sync::Mutex;

// Definiramo User strukturu kao ranije
#[derive(Debug, Serialize, Deserialize, Clone)]
struct User {
    id: u32,
    name: String,
    email: String,
}

// Definiramo AppState koji sadrži našu "bazu podataka"
struct AppState {
    users: Mutex<Vec<User>>,
    next_id: Mutex<u32>,
}

#[tokio::main]
async fn main() {
    // Inicijalizacija shared state-a
    let shared_state = Arc::new(AppState {
        users: Mutex::new(vec![]),
        next_id: Mutex::new(1),
    });

    let app = Router::new()
        .route("/users", get(get_all_users).post(create_user))
        .route("/users/:id", get(get_user_by_id).put(update_user).delete(delete_user))
        .with_state(shared_state);

    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    println!("Server sluša na {}", addr);

    axum::Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

// --- Handleri za CRUD operacije ---

// GET /users - Dohvaća sve korisnike
async fn get_all_users(State(state): State<Arc<AppState>>) -> Json<Vec<User>> {
    let users = state.users.lock().await;
    Json(users.clone())
}

// POST /users - Stvara novog korisnika
async fn create_user(State(state): State<Arc<AppState>>, Json(mut new_user): Json<User>) -> impl IntoResponse {
    let mut users = state.users.lock().await;
    let mut next_id = state.next_id.lock().await;

    new_user.id = *next_id;
    *next_id += 1;
    users.push(new_user.clone());

    (StatusCode::CREATED, Json(new_user))
}

// GET /users/:id - Dohvaća korisnika po ID-u
async fn get_user_by_id(Path(id): Path<u32>, State(state): State<Arc<AppState>>) -> impl IntoResponse {
    let users = state.users.lock().await;
    if let Some(user) = users.iter().find(|u| u.id == id) {
        (StatusCode::OK, Json(user.clone())).into_response()
    } else {
        StatusCode::NOT_FOUND.into_response()
    }
}

// PUT /users/:id - Ažurira postojećeg korisnika
async fn update_user(
    Path(id): Path<u32>,
    State(state): State<Arc<AppState>,
    Json(updated_user): Json<User>,
) -> impl IntoResponse {
    let mut users = state.users.lock().await;
    if let Some(user) = users.iter_mut().find(|u| u.id == id) {
        *user = updated_user;
        StatusCode::OK.into_response()
    } else {
        StatusCode::NOT_FOUND.into_response()
    }
}

// DELETE /users/:id - Briše korisnika po ID-u
async fn delete_user(Path(id): Path<u32>, State(state): State<Arc<AppState>>) -> impl IntoResponse {
    let mut users = state.users.lock().await;
    let initial_len = users.len();
    users.retain(|user| user.id != id);

    if users.len() < initial_len {
        StatusCode::NO_CONTENT.into_response()
    } else {
        StatusCode::NOT_FOUND.into_response()
    }
}

Objašnjenje ključnih Axum koncepata:

  • State<Arc<AppState>>: Ovo je Axumov ekstraktor za pristup zajedničkom stanju aplikacije. Axum automatski injektira ovu instancu u funkcije rukovatelja. Arc osigurava da se stanje može sigurno dijeliti među više asinkronih zadataka, dok Mutex osigurava ekskluzivan pristup podacima kada se modificiraju.
  • Json<T>: Ovaj ekstraktor automatski deserijalizira JSON tijelo zahtjeva u Rust strukturu T. Također, kada vratite Json<T> iz rukovatelja, Axum ga automatski serijalizira u JSON odgovor.
  • Path<T>: Koristi se za izdvajanje parametara iz URL putanje (npr. :id iz /users/:id).
  • impl IntoResponse: Ovaj trait omogućuje da rukovatelj vrati različite tipove odgovora (npr. (StatusCode, Json<T>), StatusCode, String, etc.). Axum zna kako ih pretvoriti u standardni HTTP odgovor.
  • axum::routing::{get, post, put, delete}: Funkcije za definiranje ruta za specifične HTTP metode.
  • .with_state(shared_state): Metoda na Router objektu koja povezuje zajedničko stanje s cijelim routerom, čineći ga dostupnim svim rukovateljima unutar tog routera.

Testiranje API-ja

Pokrenite server s cargo run. Sada možete koristiti alate poput curl ili Postman/Insomnia za testiranje API-ja:

  • GET http://localhost:3000/users (početno prazno polje)

  • POST http://localhost:3000/users

    {
        "name": "Marko Marković",
        "email": "marko@example.com"
    }
    

    (dobit ćete 201 Created s kreiranim korisnikom, npr. {"id":1,"name":"Marko Marković","email":"marko@example.com"})

  • GET http://localhost:3000/users/1 (dohvaća korisnika s ID-om 1)

  • PUT http://localhost:3000/users/1

    {
        "id": 1, 
        "name": "Marko M. Ažurirani",
        "email": "marko.updated@example.com"
    }
    

    (ažurira korisnika s ID-om 1)

  • DELETE http://localhost:3000/users/1 (briše korisnika s ID-om 1)

Napredniji Koncepti i Dobre Prakse

Error Handling

U produkcijskom API-ju, unwrap() i expect() su neprihvatljivi. Axum olakšava strukturirano rukovanje greškama koristeći Result i impl IntoResponse.

Možemo definirati prilagođeni tip greške:

// ... unutar main.rs ili u zasebnoj errors.rs datoteci

enum AppError {
    NotFound,
    InternalServerError,
    // Dodatni tipovi grešaka...
}

impl IntoResponse for AppError {
    fn into_response(self) -> axum::response::Response {
        let (status, error_message) = match self {
            AppError::NotFound => (StatusCode::NOT_FOUND, "Resource not found"),
            AppError::InternalServerError => (StatusCode::INTERNAL_SERVER_ERROR, "Internal server error"),
        };

        (status, Json(serde_json::json!({ "error": error_message }))).into_response()
    }
}

// Primjer korištenja u rukovatelju (umjesto izravnog vraćanja StatusCode::NOT_FOUND)
async fn get_user_by_id_error_handled(Path(id): Path<u32>, State(state): State<Arc<AppState>>) -> Result<Json<User>, AppError> {
    let users = state.users.lock().await;
    if let Some(user) = users.iter().find(|u| u.id == id) {
        Ok(Json(user.clone()))
    } else {
        Err(AppError::NotFound)
    }
}

S ovim pristupom, naši API odgovori s greškama postaju konzistentniji i informativniji. Također, Axum podržava Result<O, E> gdje O i E implementiraju IntoResponse.

Middleware

Axum podržava middleware za globalnu logiku kao što su logging, autentikacija, autorizacija, CORS itd. Middleware se dodaje pomoću metoda layer ili layer_fn.

Primjer logging middleware-a (zahtijeva tower-http):

# Cargo.toml

[dependencies]
...
tower-http = { version = "0.5", features = ["full"] }
use tower_http::trace::{TraceLayer, DefaultOnRequest, DefaultOnResponse};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
use http::Request;

// ... unutar main.rs, prije definicije routera

// Inicijalizacija tracinga za logging
tracing_subscriber::registry()
    .with(tracing_subscriber::EnvFilter::new(
        std::env::var("RUST_LOG")
            .unwrap_or_else(|_| "my_axum_api=debug,tower_http=debug".into()),
    ))
    .with(tracing_subscriber::fmt::layer())
    .init();

let app = Router::new()
    // ... rute ...
    .with_state(shared_state)
    .layer(
        TraceLayer::new_for_http()
            .on_request(DefaultOnRequest::new().level(tracing::Level::INFO))
            .on_response(DefaultOnResponse::new().level(tracing::Level::INFO)),
    );

// ...

Ovo će omogućiti detaljno logiranje svakog dolaznog zahtjeva i odlaznog odgovora, što je iznimno korisno za debugiranje i monitoring.

Modularizacija Ruta

Za veće aplikacije, preporučuje se modularizacija ruta. Umjesto da sve rute držite u main.rs, možete ih organizirati u zasebne module (npr., users.rs, products.rs).

// src/users.rs

use axum::{routing::{get, post, put, delete}, Router, Json};
use crate::{AppState, User, AppError};
use axum::extract::{State, Path};
use axum::http::StatusCode;
use axum::response::IntoResponse;
use std::sync::Arc;

pub fn users_routes() -> Router<Arc<AppState>> {
    Router::new()
        .route("/", get(get_all_users).post(create_user))
        .route("/:id", get(get_user_by_id).put(update_user).delete(delete_user))
}

// Premjestite handlere odavde...
// ... get_all_users, create_user, get_user_by_id, update_user, delete_user ...
// ... s potrebnim importima i ažuriranim potpisima funkcija koji primaju State ...

// src/main.rs

mod users;

// ... u main funkciji ...
let app = Router::new()
    .nest("/users", users::users_routes())
    .with_state(shared_state)
    // ... ostali slojevi ...
;

Metoda .nest() omogućuje montiranje cijelog routera na specifičnu putanju, što pomaže u održavanju čistoće i organiziranosti koda.

Zaključak

Axum je moćan i fleksibilan web framework za Rust koji omogućava izgradnju brzih, sigurnih i robusnih backend API-ja. Njegova asinkrona priroda, snažna integracija s Tokio ekosustavom i podrška za Rustov sustav tipova čine ga izvrsnim izborom za moderne web servise. Kroz ovaj vodič, istražili smo osnove postavljanja projekta, definiranja modela podataka, implementacije CRUD operacija i uvođenja naprednijih koncepata poput rukovanja greškama i middleware-a. Iako je Rustov learning curve strmiji, nagrađuje developere visokim performansama, pouzdanošću i sigurnošću koja je neusporediva s mnogim drugim jezicima.

Za daljnje učenje, preporučujem istraživanje Axum dokumentacije, Tokio ekosustava, te biblioteka za interakciju s bazama podataka kao što su sqlx (za async SQL baze podataka) ili diesel (za synchronous ORM).

Sretno kodiranje u Rustu!

Izvori i dodatno čitanje

  1. Službena Axum Dokumentacija
  2. Tokio Asinkroni Runtime
  3. Serde Framework za Serijalizaciju
  4. Tower HTTP (middleware za Axum)
B
Uredništvo portala

BAJT

Službeni autorski profil redakcije portala BAJT. Sadržaj priprema i provjerava uredništvo portala.