Lewati ke konten

Proyek: Membuat Web API dengan Axum di Rust

Di proyek ini kamu akan membuat web server sungguhan: sebuah API to-do kecil. Program lain (sebuah website, aplikasi HP, atau sekadar curl di terminal) mengirim request HTTP ke server ini, dan server menjawab dengan JSON. Inilah jenis program yang menjalankan “back end” di sebagian besar aplikasi yang kamu pakai.

Kita akan memakai axum, web framework populer yang ramah pemula, buatan tim Tokio. Axum dibangun di atas async Rust dan serde, jadi proyek ini menyatukan banyak hal yang sudah kamu pelajari: struct, Result, smart pointer, dan state yang dipakai bersama antar-thread. Kalau kamu sudah mengerjakan Daftar Tugas CLI, idenya akan terasa familier. Bedanya, kali ini daftar to-do-nya tinggal di balik sebuah alamat web.

Setelah selesai, server-mu akan menjawab request berikut:

Method dan path Fungsinya Status kalau berhasil
GET /todos Menampilkan semua to-do 200 OK
GET /todos/{id} Mengambil satu to-do 200 OK, atau 404 Not Found
POST /todos Membuat to-do dari JSON 201 Created
DELETE /todos/{id} Menghapus satu to-do 204 No Content, atau 404 Not Found

Satu to-do dalam bentuk JSON terlihat seperti ini:

{"id":1,"title":"Beli susu","done":false}

Satu catatan penting: web server menunggu request selamanya dan butuh port jaringan, jadi tidak bisa dijalankan di Rust Playground. Kode server di halaman ini dimaksudkan untuk dijalankan di komputermu sendiri dengan cargo run. Setiap langkah di bawah sudah dikompilasi dan dijalankan, dan response yang ditampilkan adalah hasil asli. Kalau ada bagian program yang bisa jalan di Playground, kamu akan menemukan tombol “Try it Yourself”.

Buat proyek baru lalu tambahkan empat crate:

Terminal window
cargo new todo-api
cd todo-api
cargo add [email protected] tokio --features tokio/full
cargo add serde --features derive
cargo add serde_json

Sekarang Cargo.toml kamu berisi kira-kira seperti ini (nomor versinya mungkin lebih baru):

[dependencies]
axum = "0.8"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }

Fungsi masing-masing:

  • axum mengurus HTTP: membaca request dan mengirim response.
  • tokio adalah async runtime yang menjalankan semuanya. Lihat Async/Await.
  • serde dan serde_json mengubah struct-mu menjadi JSON dan sebaliknya. Lihat JSON dengan Serde.

Kamu juga butuh curl untuk mengirim request. Program ini sudah ada di macOS, Linux, dan Windows 10 ke atas. (Di Windows, ketik curl.exe di PowerShell, karena di sana curl saja artinya lain.)

Ganti isi src/main.rs dengan ini:

use axum::{routing::get, Router};
#[tokio::main]
async fn main() {
let app = Router::new().route("/", get(hello));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
println!("Server berjalan di http://127.0.0.1:3000");
axum::serve(listener, app).await.unwrap();
}
async fn hello() -> &'static str {
"Halo, dunia!"
}

Ada tiga bagian yang bekerja sama di sini:

  • Handler adalah async fn yang menjawab sebuah request. hello mengembalikan teks, dan axum mengirimnya balik sebagai response.
  • Router menghubungkan path ke handler. .route("/", get(hello)) artinya “kalau ada yang mengirim request GET ke /, panggil hello”.
  • Listener membuka port 3000 di komputermu sendiri (127.0.0.1), dan axum::serve mulai menjawab request. Server terus berjalan sampai kamu menghentikannya.

Jalankan dengan cargo run. Program mencetak satu baris lalu menunggu:

Server berjalan di http://127.0.0.1:3000

Biarkan tetap berjalan, buka terminal kedua, lalu kirim request:

Terminal window
curl http://127.0.0.1:3000/
Halo, dunia!

Server tidak menambahkan baris baru setelah teksnya, jadi prompt terminal-mu mungkin muncul tepat setelah Halo, dunia!. Itu normal. Di halaman ini, setiap response ditampilkan di barisnya sendiri. Kamu juga bisa membuka http://127.0.0.1:3000/ di browser. Tekan Ctrl + C di terminal pertama untuk menghentikan server. Kamu perlu menghentikan dan menjalankan ulang server setiap kali mengubah kode.

API biasanya menjawab dengan JSON. Di axum, kamu cukup membungkus nilai apa pun yang mengimplementasikan Serialize dari serde dengan Json(...), lalu axum mengubahnya menjadi JSON dan memasang header content-type yang benar:

use axum::{routing::get, Json, Router};
use serde::Serialize;
#[derive(Serialize)]
struct Todo {
id: u32,
title: String,
done: bool,
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(hello))
.route("/example", get(example_todo));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
println!("Server berjalan di http://127.0.0.1:3000");
axum::serve(listener, app).await.unwrap();
}
async fn hello() -> &'static str {
"Halo, dunia!"
}
async fn example_todo() -> Json<Todo> {
let todo = Todo {
id: 1,
title: String::from("Belajar axum"),
done: false,
};
Json(todo)
}

Flag -i menyuruh curl ikut menampilkan baris status dan header:

Terminal window
curl -i http://127.0.0.1:3000/example
HTTP/1.1 200 OK
content-type: application/json
content-length: 44
{"id":1,"title":"Belajar axum","done":false}

(curl juga mencetak header date, yang tidak kita tampilkan di halaman ini.) Kunci-kunci JSON-nya persis sama dengan nama field di struct. Kamu bisa mengecek bentuk JSON itu di Playground tanpa server, karena ini serde biasa. Di sini juga ada struct NewTodo yang akan kita pakai di langkah 4 untuk data yang masuk:

use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize)]
struct Todo {
id: u32,
title: String,
done: bool,
}
#[derive(Debug, Deserialize)]
struct NewTodo {
title: String,
}
fn main() {
let todo = Todo { id: 1, title: String::from("Belajar axum"), done: false };
println!("{}", serde_json::to_string(&todo).unwrap());
let list = vec![todo.clone(), Todo { id: 2, title: String::from("Beli susu"), done: true }];
println!("{}", serde_json::to_string(&list).unwrap());
let input: NewTodo = serde_json::from_str(r#"{"title":"Ajak anjing jalan-jalan"}"#).unwrap();
println!("{:?}", input);
let bad = serde_json::from_str::<NewTodo>(r#"{"name":"Ajak anjing jalan-jalan"}"#);
println!("{}", bad.unwrap_err());
}

Output:

{"id":1,"title":"Belajar axum","done":false}
[{"id":1,"title":"Belajar axum","done":false},{"id":2,"title":"Beli susu","done":true}]
NewTodo { title: "Ajak anjing jalan-jalan" }
missing field `title` at line 1 column 34
▶ Coba sendiri

Baris terakhir adalah pesan error dari serde (dalam bahasa Inggris): field title tidak ditemukan di baris 1, kolom 34. Sebuah Vec<Todo> menjadi array JSON. Persis itulah yang nanti dikembalikan oleh GET /todos.

Di path seperti /todos/2, angka 2 berubah dari satu request ke request lain. Di axum 0.8, kamu menandai bagian route itu dengan kurung kurawal, {id}, lalu membacanya di handler dengan extractor Path:

use axum::{extract::Path, routing::get, Router};
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/hello/{name}", get(hello_name))
.route("/add/{a}/{b}", get(add));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
println!("Server berjalan di http://127.0.0.1:3000");
axum::serve(listener, app).await.unwrap();
}
async fn hello_name(Path(name): Path<String>) -> String {
format!("Halo, {name}!")
}
async fn add(Path((a, b)): Path<(i32, i32)>) -> String {
format!("{a} + {b} = {}", a + b)
}

Extractor adalah parameter handler yang mengambil satu bagian dari request. Path(name): Path<String> adalah sebuah pattern (pola; lihat Pattern Matching): ia membuka bungkus Path dan memberimu String di dalamnya. Dengan dua parameter, kamu mendapat sebuah tuple.

Terminal window
curl http://127.0.0.1:3000/hello/Ana
curl http://127.0.0.1:3000/add/2/3
Halo, Ana!
2 + 3 = 5

Ada kejutan menyenangkan di sini. Kamu sudah bilang a dan b bertipe i32, jadi axum memeriksanya untukmu. Coba kirim sesuatu yang bukan angka:

Terminal window
curl -i http://127.0.0.1:3000/add/2/x
HTTP/1.1 400 Bad Request
content-type: text/plain; charset=utf-8
content-length: 68
Invalid URL: Cannot parse value at index 1 with value `x` to a `i32`

Pesan dari axum ini artinya nilai x di posisi 1 tidak bisa diubah menjadi i32. Handler-mu bahkan tidak dijalankan sama sekali. Tipe-tipe di signature fungsimu itulah validasinya.

Untuk membuat to-do, client mengirim JSON di body request. Extractor Json membaca body itu dan melakukan deserialize ke struct-mu. Client hanya mengirim judul, jadi kita pakai struct NewTodo tersendiri untuk input-nya:

use axum::{routing::post, Json, Router};
use serde::{Deserialize, Serialize};
#[derive(Serialize)]
struct Todo {
id: u32,
title: String,
done: bool,
}
#[derive(Deserialize)]
struct NewTodo {
title: String,
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/echo", post(echo_todo));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
println!("Server berjalan di http://127.0.0.1:3000");
axum::serve(listener, app).await.unwrap();
}
async fn echo_todo(Json(input): Json<NewTodo>) -> Json<Todo> {
let todo = Todo {
id: 1,
title: input.title,
done: false,
};
Json(todo)
}

Pakai post(...) sebagai ganti get(...) di route-nya. Di curl, -X POST mengatur method, -H menambah header, dan -d mengirim body:

Terminal window
curl -X POST http://127.0.0.1:3000/echo -H "Content-Type: application/json" -d '{"title":"Beli susu"}'
{"id":1,"title":"Beli susu","done":false}

Sekali lagi, axum memeriksa input sebelum kodemu berjalan. Kalau JSON-nya tidak punya title, kamu mendapat 422 Unprocessable Entity:

Terminal window
curl -i -X POST http://127.0.0.1:3000/echo -H "Content-Type: application/json" -d '{"name":"Beli susu"}'
HTTP/1.1 422 Unprocessable Entity
content-type: text/plain; charset=utf-8
content-length: 99
Failed to deserialize the JSON body into the target type: missing field `title` at line 1 column 20

Dan kalau kamu lupa header Content-Type, kamu mendapat 415 Unsupported Media Type dengan pesan Expected request with `Content-Type: application/json` . Header itulah yang paling sering terlupa saat menguji dengan curl.

Langkah 5: Shared state untuk API to-do sungguhan

Section titled “Langkah 5: Shared state untuk API to-do sungguhan”

Sejauh ini, setiap handler mengarang datanya sendiri. API sungguhan perlu mengingat to-do di antara request. Request bisa datang bersamaan dan ditangani di thread yang berbeda, jadi daftarnya harus aman untuk dipakai bersama. Itulah tugas pasangan yang sudah kamu kenal di halaman Konkurensi:

  • Mutex<Vec<Todo>> memastikan hanya satu request yang mengubah daftar pada satu waktu.
  • Arc<...> membuat banyak handler bisa memiliki pointer ke daftar yang sama (lihat Smart Pointer).

Kita beri tipe itu nama pendek dengan type Db = Arc<Mutex<Vec<Todo>>>;, memberikannya ke router dengan .with_state(db), dan membacanya di handler dengan extractor State. Axum meng-clone Arc itu (murah: hanya menyalin pointer) untuk setiap request.

Ini program lengkapnya. Ganti isi src/main.rs dengan kode berikut:

use std::sync::{Arc, Mutex};
use axum::{
extract::{Path, State},
http::StatusCode,
routing::get,
Json, Router,
};
use serde::{Deserialize, Serialize};
#[derive(Clone, Serialize)]
struct Todo {
id: u32,
title: String,
done: bool,
}
#[derive(Deserialize)]
struct NewTodo {
title: String,
}
// "Database"-nya: daftar to-do yang dipakai bersama oleh semua request.
type Db = Arc<Mutex<Vec<Todo>>>;
#[tokio::main]
async fn main() {
let db: Db = Arc::new(Mutex::new(Vec::new()));
let app = Router::new()
.route("/todos", get(list_todos).post(create_todo))
.route("/todos/{id}", get(get_todo).delete(delete_todo))
.with_state(db);
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
println!("Server berjalan di http://127.0.0.1:3000");
axum::serve(listener, app).await.unwrap();
}
// GET /todos
async fn list_todos(State(db): State<Db>) -> Json<Vec<Todo>> {
let todos = db.lock().unwrap();
Json(todos.clone())
}
// GET /todos/{id}
async fn get_todo(State(db): State<Db>, Path(id): Path<u32>) -> Result<Json<Todo>, StatusCode> {
let todos = db.lock().unwrap();
match todos.iter().find(|todo| todo.id == id) {
Some(todo) => Ok(Json(todo.clone())),
None => Err(StatusCode::NOT_FOUND),
}
}
// POST /todos
async fn create_todo(State(db): State<Db>, Json(input): Json<NewTodo>) -> (StatusCode, Json<Todo>) {
let mut todos = db.lock().unwrap();
let id = todos.iter().map(|todo| todo.id).max().unwrap_or(0) + 1;
let todo = Todo {
id,
title: input.title,
done: false,
};
todos.push(todo.clone());
(StatusCode::CREATED, Json(todo))
}
// DELETE /todos/{id}
async fn delete_todo(State(db): State<Db>, Path(id): Path<u32>) -> StatusCode {
let mut todos = db.lock().unwrap();
let before = todos.len();
todos.retain(|todo| todo.id != id);
if todos.len() < before {
StatusCode::NO_CONTENT
} else {
StatusCode::NOT_FOUND
}
}

Mari kita bahas ide-ide barunya:

  • Satu path, beberapa method. get(list_todos).post(create_todo) mengirim GET /todos dan POST /todos ke handler yang berbeda.
  • Status code adalah nilai kembalian. StatusCode berasal dari modul http milik axum. Handler bisa mengembalikan Result: Ok(Json(todo)) menjadi 200 dengan JSON, dan Err(StatusCode::NOT_FOUND) menjadi 404 yang kosong. Tuple (StatusCode::CREATED, Json(todo)) mengatur status dan body sekaligus. Mengembalikan StatusCode::NO_CONTENT (204) saja artinya “beres, tidak ada yang perlu dikirim balik”.
  • lock() dan guard-nya. db.lock().unwrap() menunggu sampai tidak ada request lain yang memakai daftar itu, lalu memberimu sebuah guard yang bisa kamu baca dan ubah seperti Vec biasa. Kunci dilepas saat guard keluar dari scope (cakupan) di akhir fungsi.
  • Id baru. Id berikutnya adalah id terbesar di daftar ditambah satu, atau 1 kalau daftarnya kosong.
  • Menghapus. retain menyimpan semua to-do yang id-nya tidak cocok. Kalau panjangnya tidak berubah, berarti tidak ada yang terhapus, jadi kita kembalikan 404.

Kenapa std::sync::Mutex dan bukan versi async? Tidak masalah (dan malah lebih cepat) selama kamu tidak memegang kunci melewati sebuah .await. Tidak ada handler kita yang melakukannya: semuanya mengunci, mengolah daftar, lalu selesai.

Logika daftar-dan-kunci ini tidak butuh web server, jadi bisa kamu coba di Playground. Fungsi-fungsi berikut melakukan pekerjaan yang sama dengan handler-nya, tanpa axum:

use std::sync::{Arc, Mutex};
#[derive(Clone, Debug)]
struct Todo {
id: u32,
title: String,
done: bool,
}
type Db = Arc<Mutex<Vec<Todo>>>;
fn create(db: &Db, title: &str) -> Todo {
let mut todos = db.lock().unwrap();
let id = todos.iter().map(|todo| todo.id).max().unwrap_or(0) + 1;
let todo = Todo { id, title: title.to_string(), done: false };
todos.push(todo.clone());
todo
}
fn get(db: &Db, id: u32) -> Option<Todo> {
let todos = db.lock().unwrap();
todos.iter().find(|todo| todo.id == id).cloned()
}
fn delete(db: &Db, id: u32) -> bool {
let mut todos = db.lock().unwrap();
let before = todos.len();
todos.retain(|todo| todo.id != id);
todos.len() < before
}
fn main() {
let db: Db = Arc::new(Mutex::new(Vec::new()));
let second_handle = Arc::clone(&db); // yang dilakukan axum untuk setiap request
println!("{:?}", create(&db, "Beli susu"));
println!("{:?}", create(&second_handle, "Belajar axum"));
println!("{:?}", get(&db, 2));
println!("{:?}", get(&db, 99));
println!("1 terhapus? {}", delete(&db, 1));
println!("1 terhapus lagi? {}", delete(&db, 1));
println!("sisa: {}", db.lock().unwrap().len());
}

Output:

Todo { id: 1, title: "Beli susu", done: false }
Todo { id: 2, title: "Belajar axum", done: false }
Some(Todo { id: 2, title: "Belajar axum", done: false })
None
1 terhapus? true
1 terhapus lagi? false
sisa: 1
▶ Coba sendiri

To-do yang dibuat lewat second_handle ikut muncul saat kamu membaca lewat db, karena kedua Arc menunjuk ke daftar yang sama.

Jalankan server dengan cargo run, lalu di terminal kedua, coba semua endpoint. Awalnya daftarnya kosong:

Terminal window
curl http://127.0.0.1:3000/todos
[]

Buat sebuah to-do. Perhatikan status 201 Created-nya:

Terminal window
curl -i -X POST http://127.0.0.1:3000/todos -H "Content-Type: application/json" -d '{"title":"Beli susu"}'
HTTP/1.1 201 Created
content-type: application/json
content-length: 41
{"id":1,"title":"Beli susu","done":false}

Buat satu lagi, lalu tampilkan semuanya:

Terminal window
curl -X POST http://127.0.0.1:3000/todos -H "Content-Type: application/json" -d '{"title":"Belajar axum"}'
curl http://127.0.0.1:3000/todos
{"id":2,"title":"Belajar axum","done":false}
[{"id":1,"title":"Beli susu","done":false},{"id":2,"title":"Belajar axum","done":false}]

Ambil satu berdasarkan id, lalu minta yang tidak ada:

Terminal window
curl http://127.0.0.1:3000/todos/2
curl -i http://127.0.0.1:3000/todos/99
{"id":2,"title":"Belajar axum","done":false}
HTTP/1.1 404 Not Found
content-length: 0

Hapus to-do 1, lalu coba hapus sekali lagi:

Terminal window
curl -i -X DELETE http://127.0.0.1:3000/todos/1
curl -i -X DELETE http://127.0.0.1:3000/todos/1
HTTP/1.1 204 No Content
HTTP/1.1 404 Not Found
content-length: 0
Terminal window
curl http://127.0.0.1:3000/todos
[{"id":2,"title":"Belajar axum","done":false}]

Axum juga menangani beberapa kesalahan untukmu. Tipe yang salah di JSON menghasilkan 422 yang menjelaskan masalahnya (di sini: title seharusnya string, bukan integer), dan method yang tidak kamu siapkan untuk suatu path menghasilkan 405 Method Not Allowed, dengan header yang mencantumkan method yang bisa dipakai:

Terminal window
curl -i -X POST http://127.0.0.1:3000/todos -H "Content-Type: application/json" -d '{"title": 42}'
curl -i -X PUT http://127.0.0.1:3000/todos/2
HTTP/1.1 422 Unprocessable Entity
content-type: text/plain; charset=utf-8
content-length: 130
Failed to deserialize the JSON body into the target type: title: invalid type: integer `42`, expected a string at line 1 column 12
HTTP/1.1 405 Method Not Allowed
allow: GET,HEAD,DELETE
content-length: 0

Hentikan server dengan Ctrl + C lalu jalankan lagi: daftarnya kosong. Datanya hanya tinggal di memori. Menyimpannya adalah ide pertama di bawah.

Memakai sintaks route lama :id. Banyak tutorial di internet ditulis untuk axum 0.7, yang memakai /todos/:id. Di axum 0.8 kode ini tetap bisa dikompilasi, tapi programnya panic begitu dijalankan:

thread 'main' (12345) panicked at src/main.rs:32:10:
Path segments must not start with `:`. For capture groups, use `{capture}`. If you meant to literally match a segment starting with a colon, call `without_v07_checks` on the router.

Pesannya bilang segmen path tidak boleh diawali : dan menyarankan {capture}. Solusinya: tulis /todos/{id}.

Menaruh Json sebelum extractor lain. Body request hanya bisa dibaca sekali, jadi extractor yang membacanya (Json) harus menjadi parameter terakhir. Kalau kamu menulis create_todo(Json(input): Json<NewTodo>, State(db): State<Db>), pesan error-nya tidak langsung bilang begitu. Baris-baris pentingnya adalah:

error[E0277]: the trait bound `fn(Json<NewTodo>, State<...>) -> ... {create_todo}: Handler<_, ...>` is not satisfied
|
31 | .route("/todos", get(list_todos).post(create_todo))
| ---- ^^^^^^^^^^^ unsatisfied trait bound
|
= note: Consider using `#[axum::debug_handler]` to improve the error message

Kalau kamu melihat “Handler is not satisfied” (handler-nya tidak memenuhi trait Handler), periksa urutan parameter, dan pastikan setiap tipe kembalian adalah sesuatu yang bisa diubah axum menjadi response.

Lupa -H "Content-Type: application/json" di curl. Server membalas 415. Header ini wajib untuk extractor Json.

Lupa menjalankan ulang. Server yang sedang berjalan masih program yang lama. Hentikan dengan Ctrl + C lalu cargo run lagi setiap kali ada perubahan.

Setelah API dasarnya jalan, ini beberapa langkah berikutnya yang bagus. Coba satu per satu.

  • Tandai to-do sebagai selesai. Tambahkan PATCH /todos/{id} dengan .patch(...) di route kedua. Cari to-do-nya dengan iter_mut(), set done = true, lalu kembalikan.
  • Validasi. Judul kosong seharusnya tidak boleh. Handler bisa mengembalikan Result<(StatusCode, Json<Todo>), (StatusCode, String)>, dan mengirim balik Err((StatusCode::BAD_REQUEST, String::from("judul tidak boleh kosong"))). Pakai title.trim().is_empty() untuk mengeceknya.
  • Id yang tidak pernah berulang. Dengan cara “id terbesar ditambah satu”, menghapus to-do terbaru membuat id-nya bisa dipakai lagi. Sebagai gantinya, simpan penghitung terpisah di state-mu, misalnya struct berisi todos: Vec<Todo> dan next_id: u32 di dalam Mutex.
  • Simpan permanen ke file JSON. Tambahkan juga derive Deserialize pada Todo. Saat server mulai, baca todos.json dengan std::fs::read_to_string dan serde_json::from_str (atau mulai dari kosong kalau file-nya belum ada). Setiap kali ada perubahan, tulis daftarnya kembali dengan serde_json::to_string_pretty dan std::fs::write. Halaman Baca-Tulis File dan JSON dengan Serde berisi semua yang kamu butuhkan. Nantinya kamu bisa mengganti file itu dengan database sungguhan seperti SQLite.
  • Logging dengan tower-http. Jalankan cargo add tower-http --features trace dan cargo add tracing-subscriber, lalu tambahkan layer logging. Sekarang server mencetak satu baris untuk setiap request, berisi method, path, status, dan berapa lama prosesnya:
use tower_http::trace::TraceLayer;
// di awal main:
tracing_subscriber::fmt()
.with_max_level(tracing_subscriber::filter::LevelFilter::DEBUG)
.init();
// saat membuat router:
let app = Router::new()
.route("/todos", get(list_todos).post(create_todo))
.route("/todos/{id}", get(get_todo).delete(delete_todo))
.layer(TraceLayer::new_for_http())
.with_state(db);
  • Catatan untuk deployment. 127.0.0.1 hanya menerima request dari komputermu sendiri. Untuk menerima request dari mesin lain (misalnya di dalam Docker atau di server), bind ke 0.0.0.0. Banyak layanan hosting memberi tahu programmu port mana yang dipakai lewat environment variable PORT, yang bisa kamu baca dengan std::env::var (lihat Environment Variable dan Proses). Build dengan cargo build --release untuk server yang jauh lebih cepat. Hasilnya adalah satu file di target/release/ yang bisa kamu salin ke server Linux, atau masukkan ke image Docker yang kecil. Untuk situs publik, taruh di belakang reverse proxy (seperti Caddy atau nginx) yang mengurus HTTPS.
  • Handler adalah sebuah async fn. Router memetakan method dan path ke handler. axum::serve menjalankannya di atas TcpListener milik tokio.
  • Extractor mengambil data dari request: Path untuk bagian URL (/todos/{id}), Json untuk body (selalu paling akhir), dan State untuk data yang dipakai bersama.
  • Kembalikan Json(value) untuk JSON, StatusCode untuk status, tuple untuk mengatur keduanya, atau Result untuk memilih antara sukses dan status error.
  • Arc<Mutex<Vec<Todo>>> membagi satu daftar dengan aman ke semua request. Kunci, pakai, lalu biarkan guard-nya di-drop. Jangan memegangnya melewati .await.
  • Uji API-mu dengan curl: -i menampilkan status, -X mengatur method, -H menambah header, dan -d mengirim body.

Kuis singkat

1. Di axum 0.8, bagaimana cara menulis route dengan id di path-nya?

2. Sebuah handler mengembalikan (StatusCode::CREATED, Json(todo)). Apa yang diterima client?

3. Kenapa daftar yang dipakai bersama dibungkus dengan Arc<Mutex<...>>?

4. Extractor mana yang harus ada di urutan terakhir parameter handler?

5. Kamu mengirim JSON dengan curl -X POST tapi lupa header Content-Type: application/json. Apa balasan axum?

Skor: 0 / 5