Sadržaj članka
- Uvod u Axum i Rust za Backend Razvoj
- Postavljanje Okoline i Projekta
- Inicijalizacija Projekta
- Osnovna “Hello World” Aplikacija
- Definiranje Modela Podataka s Serde
- Implementacija CRUD Operacija (Stvaranje, Čitanje, Ažuriranje, Brisanje)
- Upravljanje Stanstvom Aplikacije (Shared State)
- Objašnjenje ključnih Axum koncepata:
- Testiranje API-ja
- Napredniji Koncepti i Dobre Prakse
- Error Handling
- Middleware
- Modularizacija Ruta
- 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 pretvaramainfunkciju u asinkronu i inicijalizira Tokio runtime, omogućujući nam korištenjeawaitključne riječi.Router::new()stvara instancu routera..route("/", get(handler))definira rutu zaGETzahtjeve na putanji/i povezuje je s funkcijomhandler.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)]:Debugje koristan za ispis u konzolu,SerializeiDeserializesu ključni za JSON, aCloneje č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.Arcosigurava da se stanje može sigurno dijeliti među više asinkronih zadataka, dokMutexosigurava ekskluzivan pristup podacima kada se modificiraju.Json<T>: Ovaj ekstraktor automatski deserijalizira JSON tijelo zahtjeva u Rust strukturuT. Također, kada vratiteJson<T>iz rukovatelja, Axum ga automatski serijalizira u JSON odgovor.Path<T>: Koristi se za izdvajanje parametara iz URL putanje (npr.:idiz/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 naRouterobjektu 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!
Komentari