Najvažnije iz članka
- Rust nudi iznimne performanse, sigurnost memorije i konkurentnost idealne za backend API-je, eliminirajući uobičajene greške poput data races.
- Axum je fleksibilan i modularan web framework za Rust, izgrađen na Tokio runtimeu, koji koristi type-safe extractore i moćan middleware sustav.
- Tutorial demonstrira izgradnju RESTful API-ja, uključujući rukovanje JSON bodyjem, path parametrima i dodavanje logging middlewarea pomoću `tower-http`.
- Uključena je integracija s asinkronim ORM-om `SQLx` za `compile-time` provjeru SQL upita i interakciju s bazom podataka (SQLite primjer).
Sadržaj članka
- Zašto Rust za Backend Development?
- Uvod u Axum Framework
- Postavljanje Projekta
- Izgradnja Jednostavnog API-ja
- Objašnjenje Koda:
- Rad s JSON Podacima i Path Parametrima
- Prihvaćanje JSON Podataka (POST Request)
- Čitanje Path Parametara
- Korištenje Middleware-a
- Implementacija Baze Podataka (SQLx)
- Objašnjenje promjena:
- Zaključak
U posljednjih nekoliko godina, Rust je dramatično porastao u popularnosti kao programski jezik izbora za sistemsko programiranje, ali i sve više za backend web development. Njegova reputacija za performanse, sigurnost memorije i konkurentnost privlači pažnju developera koji traže robustna rješenja za izgradnju skalabilnih i pouzdanih API-ja. U ovom detaljnom tutorialu, istražit ćemo zašto je Rust odličan izbor za backend, te kako ga koristiti s Axum web frameworkom za izgradnju visokoperformansnih API-ja.
Zašto Rust za Backend Development?
Rust nudi jedinstvenu kombinaciju značajki koje ga čine izuzetno privlačnim za razvoj backend servisa:
- Izvanredne Performanse: Rust je dizajniran da bude brz. S minimalnim runtimeom i bez garbage collectiona, omogućuje kontrolu niske razine sličnu C++-u, ali s mnogo većom sigurnošću. To rezultira ekstremno niskom latencijom i visokom propusnošću, ključnim za zahtjevne API-je.
- Sigurnost Memorije: Rustov sustav ownershipa i borrowinga eliminira čitavu klasu bugova povezanih s memorijom (null pointer dereferenciranje, data races, use-after-free) na razini kompajlera. To smanjuje potrebu za opsežnim runtime provjerama i čini aplikacije stabilnijima.
- Konkurentnost bez Data Races: Rustov ownership model osigurava da
data racesnisu mogućiunsafekonteksta, što olakšava pisanje sigurnog konkurentnog koda bez straha od uobičajenih pogrešaka.Tokioekosustav, na kojem se Axum temelji, pruža snažne alate za asinkrono programiranje. - Robusni Ekosustav: Rust ima brzorastući ekosustav biblioteka (
crates.io), uključujući snažne alate za web development (Tokio, reqwest, serde, sqlx, Axum, actix-web).Cargo, Rustov sustav za izgradnju i upravljanje paketima, je iznimno moćan i jednostavan za korištenje. - Razvojna Produktivnost: Iako Rust ima strmu krivulju učenja, jednom kada se savladaju njegovi osnovni koncepti, razvoj postaje vrlo produktivan. Kompajler je izuzetno koristan i detaljan u svojim porukama o pogreškama, što pomaže u brzom rješavanju problema.
Uvod u Axum Framework
Axum je web framework za Rust izgrađen na Tokio middleware ekosustavu. Ističe se po svojoj modularnosti, fleksibilnosti i snažnoj integraciji s Tokio asinkronim runtimeom. Ključne značajke Axuma uključuju:
- Tokio-native: Potpuno iskorištava Tokio za asinkrono I/O i konkurentnost.
- Middleware sustav: Omogućuje jednostavnu integraciju
middlewareaza logiranje, autentikaciju, kompresiju i druge funkcionalnosti. - Type-safe Extractors: Koristi
extractoreza sigurno izdvajanje podataka izrequesta(npr. JSON body, query parametri, path parametri). - Minimalan API: Manje boilerplate koda u usporedbi s nekim drugim frameworkovima, održavajući fleksibilnost.
- Potpuno asinkron: Izgrađen od temelja za asinkrono izvršavanje, što ga čini idealnim za I/O intenzivne aplikacije.
Postavljanje Projekta
Prije nego što počnemo, osigurajmo da imamo instaliran Rust i Cargo. Ako niste, posjetite rustup.rs i slijedite upute.
Kreirajmo novi Rust projekt:
cargo new rust-axum-api --bin
cd rust-axum-api
Sada moramo dodati potrebne dependencies u naš Cargo.toml datoteku:
[package]
name = "rust-axum-api"
version = "0.1.0"
edition = "2021"
[dependencies]
axum = "0.7"
tokio = { version = "1.37", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
axum: Glavni web framework.tokio: Asinkroni runtime, sfullznačajkom za sve uobičajene module.serde: Biblioteka za serializaciju/deserializaciju Rust struktura u/iz različitih formata (poput JSON-a).serde_json: Specifična implementacija Serde za JSON format.
Izgradnja Jednostavnog API-ja
Počnimo s jednostavnim API-jem koji će imati jednu rutu za dohvaćanje poruke dobrodošlice.
Otvorite src/main.rs i dodajte sljedeći kod:
use axum::{routing::get, Json, Router};
use serde::{Deserialize, Serialize};
use std::net::SocketAddr;
#[tokio::main]
async fn main() {
// Definiranje ruta
let app = Router::new()
.route("/", get(root))
.route("/hello", get(hello_world));
// Postavljanje adrese servera
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
println!("Listening on http://{}", addr);
// Pokretanje servera
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
async fn root() -> &'static str {
"Welcome to Axum API!"
}
// Definiranje strukture za odgovor
#[derive(Serialize)]
struct Message {
message: String,
}
async fn hello_world() -> Json<Message> {
let msg = Message {
message: "Hello from Axum!".to_string(),
};
Json(msg)
}
Objašnjenje Koda:
#[tokio::main]: Makro koji označava glavnu asinkronu funkciju i inicijaliziraTokioruntime.Router::new(): Kreira novu instancuRouterakoji mapirarequestenahandlere..route("/", get(root)): Definira GET rutu za/putanju koja će pozvatirootfunkciju.SocketAddr::from(([127, 0, 0, 1], 3000)): KreiraSocketAddrzalocalhostna portu3000.axum::Server::bind(&addr).serve(app.into_make_service()).await.unwrap();: Pokreće HTTP server.into_make_service()pretvaraRouteru nešto što server može servisirati.root()funkcija: Jednostavna asinkrona funkcija koja vraća statični string.#[derive(Serialize)]: Atribut izSerdebiblioteke koji automatski generira kod za serializacijuMessagestrukture u format kao što je JSON.hello_world()funkcija: VraćaJson<Message>. AxumovJsonextractor/responder automatski serializiraMessagestrukturu u JSON i postavljaContent-Type: application/jsonzaglavlje.
Pokrenite aplikaciju:
cargo run
Posjetite http://127.0.0.1:3000/ u vašem pregledniku ili koristite curl:
curl http://127.0.0.1:3000/
# Očekivani izlaz: Welcome to Axum API!
curl http://127.0.0.1:3000/hello
# Očekivani izlaz: {"message":"Hello from Axum!"}
Rad s JSON Podacima i Path Parametrima
Većina API-ja radi s dinamičkim podacima. Pogledajmo kako Axum obrađuje JSON request tijela i path parametre.
Prihvaćanje JSON Podataka (POST Request)
Dodajmo novu rutu koja prima JSON podatke i vraća ih natrag, simulirajući stvaranje resursa.
Prvo, trebamo strukturu za request tijelo. Dodajte Deserialize atribut za Message strukturu, ili kreirajte novu:
// ... (ostali use statements)
#[derive(Debug, Deserialize, Serialize)]
struct CreateUser {
username: String,
email: String,
}
#[derive(Debug, Serialize)]
struct User {
id: u64,
username: String,
email: String,
}
async fn create_user(Json(payload): Json<CreateUser>) -> Json<User> {
// U stvarnoj aplikaciji ovdje biste spremili korisnika u bazu podataka
// i generirali stvarni ID.
let id = 123; // Fiksni ID za primjer
let user = User {
id,
username: payload.username,
email: payload.email,
};
println!("Created user: {:#?}", user);
Json(user)
}
// Ažurirajte main funkciju da uključi novu rutu:
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(root))
.route("/hello", get(hello_world))
.route("/users", post(create_user)); // Dodajemo POST rutu
// ... (ostatak main funkcije)
}
Ovdje koristimo Json<CreateUser> kao tip argumenta u create_user funkciji. Axum će automatski pokušati deserializirati request tijelo u CreateUser strukturu. Ako to ne uspije (npr. zbog neispravnog JSON formata ili nedostajućih polja), Axum će automatski vratiti 400 Bad Request odgovor.
Testiranje s curl:
curl -X POST -H "Content-Type: application/json" -d '{"username":"john.doe", "email":"john@example.com"}' http://127.0.0.1:3000/users
# Očekivani izlaz: {"id":123,"username":"john.doe","email":"john@example.com"}
Čitanje Path Parametara
Često nam je potrebno dohvatiti specifični resurs koristeći ID u URL-u. Axum to olakšava s Path extractorom.
use axum::extract::Path; // Dodajte ovaj use statement
// ... (ostale strukture)
async fn get_user_by_id(Path(user_id): Path<u64>) -> String {
// U stvarnoj aplikaciji, dohvatiti korisnika iz baze podataka
// koristeći user_id.
format!("Fetching user with ID: {}", user_id)
}
// Ažurirajte main funkciju da uključi novu rutu:
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(root))
.route("/hello", get(hello_world))
.route("/users", post(create_user))
.route("/users/:id", get(get_user_by_id)); // Ruta s path parametrom
// ... (ostatak main funkcije)
}
Testiranje:
curl http://127.0.0.1:3000/users/42
# Očekivani izlaz: Fetching user with ID: 42
curl http://127.0.0.1:3000/users/abc
# Očekivani izlaz: (Axum će vratiti 400 Bad Request jer 'abc' nije u64)
Primijetite kako Axum automatski parsira user_id kao u64. Ako parsiranje ne uspije, Axum će automatski vratiti prikladan HTTP status kod.
Korištenje Middleware-a
Middleware je moćan koncept za dodavanje zajedničke funkcionalnosti svim ili određenim rutama (npr. logiranje, autentikacija, CORS). Axum koristi tower-http biblioteku za mnoge uobičajene middleware implementacije.
Dodajmo logiranje za sve requeste:
Prvo, dodajmo tower-http u Cargo.toml:
[dependencies]
# ...
tower-http = { version = "0.5", features = ["trace"] }
Zatim, modificirajte src/main.rs:
use axum::{routing::{get, post}, Json, Router};
use axum::extract::Path;
use serde::{Deserialize, Serialize};
use std::net::SocketAddr;
use tower_http::trace::TraceLayer; // Dodajte ovo
// ... (strukture i handleri)
#[tokio::main]
async fn main() {
// Inicijalizirajte tracing za bolje logiranje
tracing_subscriber::fmt()
.with_max_level(tracing::Level::INFO)
.init();
let app = Router::new()
.route("/", get(root))
.route("/hello", get(hello_world))
.route("/users", post(create_user))
.route("/users/:id", get(get_user_by_id))
.layer(TraceLayer::new_for_http()); // Dodajte TraceLayer kao middleware
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
tracing::info!("Listening on http://{}", addr); // Koristite tracing::info umjesto println!
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
Dodali smo tower_http::trace::TraceLayer koji omogućuje automatsko logiranje HTTP requesta i responsea. Također smo uključili tracing_subscriber za bolje formatiranje logova.
Dodajte i tracing u Cargo.toml:
[dependencies]
# ...
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["fmt"] }
Sada, kada pokrenete aplikaciju i šaljete requeste, vidjet ćete detaljne logove u konzoli:
cargo run
INFO rust_axum_api: Listening on http://127.0.0.1:3000
INFO tower_http::trace::make_span: GET / - started
INFO tower_http::trace::make_span: GET / - finished in 18µs
Ovo je samo jedan primjer middlewarea. tower-http nudi mnoge druge korisne middlewaree poput CORS, compression, timeout i rate limiting.
Implementacija Baze Podataka (SQLx)
U većini realnih aplikacija, trebat će nam interakcija s bazom podataka. SQLx je asinkroni, compile-time provjereni SQL crate za Rust koji podržava PostgreSQL, MySQL, SQLite i MSSQL. Koristit ćemo SQLite za jednostavnost, ali principi su slični za druge baze podataka.
Dodajte SQLx u Cargo.toml:
[dependencies]
# ...
sqlx = { version = "0.7", features = ["runtime-tokio", "sqlite", "macros", "chrono"] }
chrono = { version = "0.4", features = ["serde"] }
Stvorimo SQLite bazu podataka i tablicu. U src/main.rs, modificirajte main funkciju:
// ... (use statements)
use sqlx::{sqlite::SqlitePool, Pool, Sqlite};
use chrono::prelude::*;
// ... (strukture CreateUser, User)
#[derive(Debug, Serialize, Deserialize, Clone)] // Dodajte Clone za lakše prosljeđivanje u handleru
struct User {
id: i64,
username: String,
email: String,
created_at: DateTime<Utc>,
}
// Kreirajte globalni pool za bazu podataka
// U Axum aplikacijama, to obično radite putem Extensiona
#[tokio::main]
async fn main() {
// ... (tracing init)
let database_url = "sqlite://sqlite.db"; // Putanja do SQLite baze podataka
let pool = SqlitePool::connect(database_url).await.expect("Failed to connect to database");
// Pokrenite migracije ili kreirajte tablicu ako ne postoji
sqlx::migrate!("./migrations")
.run(&pool)
.await
.expect("Failed to run migrations");
let app = Router::new()
.route("/", get(root))
.route("/hello", get(hello_world))
.route("/users", post(create_user_db))
.route("/users/:id", get(get_user_by_id_db))
.layer(TraceLayer::new_for_http())
.with_state(pool); // Prosljeđivanje poola kao stanja aplikacije
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
tracing::info!("Listening on http://{}", addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
// Ažurirajte handlere da koriste bazu podataka
use axum::extract::State;
async fn create_user_db(
State(pool): State<SqlitePool>,
Json(payload): Json<CreateUser>,
) -> Result<Json<User>, (axum::http::StatusCode, String)> {
let now = Utc::now();
let res = sqlx::query_as!(User,
"INSERT INTO users (username, email, created_at) VALUES ($1, $2, $3) RETURNING id, username, email, created_at",
payload.username,
payload.email,
now
)
.fetch_one(&pool)
.await;
match res {
Ok(user) => Ok(Json(user)),
Err(e) => Err((axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to create user: {}", e))),
}
}
async fn get_user_by_id_db(
State(pool): State<SqlitePool>,
Path(user_id): Path<i64>,
) -> Result<Json<User>, (axum::http::StatusCode, String)> {
let res = sqlx::query_as!(User,
"SELECT id, username, email, created_at FROM users WHERE id = $1",
user_id
)
.fetch_optional(&pool)
.await;
match res {
Ok(Some(user)) => Ok(Json(user)),
Ok(None) => Err((axum::http::StatusCode::NOT_FOUND, "User not found".to_string())),
Err(e) => Err((axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to fetch user: {}", e))),
}
}
Objašnjenje promjena:
Struktura
User: Dodali smoidicreated_atpolja koja će biti generirana u bazi podataka. AtributCloneje dodan jerStateponekad zahtijeva da tip budeClone.SqlitePool: InicijaliziramoSqlitePooli povezujemo se nasqlite.dbdatoteku.expectse koristi za jednostavnost, u produkciji biste koristili robusnije rukovanje greškama.Migracije:
sqlx::migrate!("./migrations").run(&pool)automatski pokreće SQL migracije izmigrationsdirektorija. Kreirajte/migrationsdirektorij u korijenu projekta i unutar njega datoteku20231027_init_users_table.sql(ime datoteke mora početi s datumom/vremenom):-- migrations/20231027_init_users_table.sql CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, email TEXT NOT NULL UNIQUE, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP );with_state(pool): Ovo prosljeđujeSqlitePoolinstancu kao stanje dostupno svimhandlerima.Stateextractor se koristi za pristup tom stanju unutarhandler funkcija.create_user_dbiget_user_by_id_db: Ove funkcije sada koristeState<SqlitePool>isqlx::query_as!makro za sigurnu interakciju s bazom podataka.query_as!je moćan jer provjerava SQL upite ucompile-timeu.
Sada možete ponovno pokrenuti aplikaciju i testirati API za korisnike:
cargo run
Kreiranje korisnika:
curl -X POST -H "Content-Type: application/json" -d '{"username":"jane.doe", "email":"jane@example.com"}' http://127.0.0.1:3000/users
# Očekivani izlaz: {"id":1,"username":"jane.doe","email":"jane@example.com","created_at":"2023-10-27T10:00:00Z"}
Dohvaćanje korisnika:
curl http://127.0.0.1:3000/users/1
# Očekivani izlaz: {"id":1,"username":"jane.doe","email":"jane@example.com","created_at":"2023-10-27T10:00:00Z"}
Zaključak
Rust, u kombinaciji s Axum frameworkom, nudi izvanredan spoj performansi, sigurnosti i razvojne produktivnosti za izgradnju backend API-ja. Njegov naglasak na sigurnosti memorije eliminira mnoge uobičajene greške koje se javljaju u drugim jezicima, dok asinkroni ekosustav na bazi Tokija omogućuje efikasno rukovanje visokim brojem istodobnih requesta.
Iako početna krivulja učenja Rusta može biti strma, nagrada je izuzetno robustna, brza i skalabilna aplikacija koja može izdržati najzahtjevnije radne uvjete. Integracija s bibliotekama poput Serde za serializaciju/deserializaciju i SQLx za interakciju s bazom podataka čini Rust kompletno rješenjem za moderno backend development. Ako tražite jezik i framework za izgradnju performantnog i pouzdanog API-ja, Rust i Axum svakako zaslužuju vašu pažnju.
Izazov za čitatelje: Pokušajte dodati funkcionalnost za ažuriranje (PUT) i brisanje (DELETE) korisnika, uključujući odgovarajuće SQL izraze i rukovanje greškama.
Komentari