Ukratko

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
  1. Zašto Rust za Backend Development?
  2. Uvod u Axum Framework
  3. Postavljanje Projekta
  4. Izgradnja Jednostavnog API-ja
  5. Objašnjenje Koda:
  6. Rad s JSON Podacima i Path Parametrima
  7. Prihvaćanje JSON Podataka (POST Request)
  8. Čitanje Path Parametara
  9. Korištenje Middleware-a
  10. Implementacija Baze Podataka (SQLx)
  11. Objašnjenje promjena:
  12. 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 races nisu mogući unsafe konteksta, što olakšava pisanje sigurnog konkurentnog koda bez straha od uobičajenih pogrešaka. Tokio ekosustav, 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 middlewarea za logiranje, autentikaciju, kompresiju i druge funkcionalnosti.
  • Type-safe Extractors: Koristi extractore za sigurno izdvajanje podataka iz requesta (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, s full znač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 inicijalizira Tokio runtime.
  • Router::new(): Kreira novu instancu Routera koji mapira requeste na handlere.
  • .route("/", get(root)): Definira GET rutu za / putanju koja će pozvati root funkciju.
  • SocketAddr::from(([127, 0, 0, 1], 3000)): Kreira SocketAddr za localhost na portu 3000.
  • axum::Server::bind(&addr).serve(app.into_make_service()).await.unwrap();: Pokreće HTTP server. into_make_service() pretvara Router u nešto što server može servisirati.
  • root() funkcija: Jednostavna asinkrona funkcija koja vraća statični string.
  • #[derive(Serialize)]: Atribut iz Serde biblioteke koji automatski generira kod za serializaciju Message strukture u format kao što je JSON.
  • hello_world() funkcija: Vraća Json<Message>. Axumov Json extractor/responder automatski serializira Message strukturu u JSON i postavlja Content-Type: application/json zaglavlje.

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:

  1. Struktura User: Dodali smo id i created_at polja koja će biti generirana u bazi podataka. Atribut Clone je dodan jer State ponekad zahtijeva da tip bude Clone.

  2. SqlitePool: Inicijaliziramo SqlitePool i povezujemo se na sqlite.db datoteku. expect se koristi za jednostavnost, u produkciji biste koristili robusnije rukovanje greškama.

  3. Migracije: sqlx::migrate!("./migrations").run(&pool) automatski pokreće SQL migracije iz migrations direktorija. Kreirajte /migrations direktorij u korijenu projekta i unutar njega datoteku 20231027_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
    );
    
  4. with_state(pool): Ovo prosljeđuje SqlitePool instancu kao stanje dostupno svim handlerima. State extractor se koristi za pristup tom stanju unutar handler funkcija.

  5. create_user_db i get_user_by_id_db: Ove funkcije sada koriste State<SqlitePool> i sqlx::query_as! makro za sigurnu interakciju s bazom podataka. query_as! je moćan jer provjerava SQL upite u compile-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.

Izvori i dodatno čitanje

  1. Axum službena dokumentacija
  2. Tokio: An Asynchronous Runtime for Rust
  3. SQLx: The Rust SQL Toolkit
  4. Rust Programming Language
B
Uredništvo portala

BAJT

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