Pengalaman Pertama, Membuat API dengan Rust

Pengalaman Pertama, Membuat API dengan Rust

Zakiego

Zakiego

@zakiego

Catatan

Tulisan ini bukan merupakan tutorial, namun lebih banyak mengandung hikmah dan petuah. Banyak konsep yang disimplifikasi, serta tentu saja, banyak kode yang perlu diperbaiki dan ditingkatkan. Ini bukan sebuah best practice. Silakan explore lebih jauh.

Abstrak

Setelah banyak kicauan dari para penganut Rust, saya mulai tertarik kembali untuk bermain Rust. Project yang coba dibuat adalah dengan menggunakan Axum sebagai framework backend, dan SQLX sebagai SQL toolkit. Banyak hal-hal kecil yang kemudian dipelajari. Dan tentu saja, banyak yang perlu ditingkatkan.

Kode masih dalam pengembangan, bisa dilihat di github.com/zakiego/social-axum serta bisa diakses melalui axum.zakiego.com.

Latar Belakang

Belakangan ini, Rust kembali sering muncul di timeline saya. Setidaknya ada dua pemain utama Rust yang berseliweran, yaitu @papanberjalan dan @mustafasegf.

Jika kamu mengira ini pengalaman pertama saya belajar Rust, kamu salah. Saya pertama kali belajar Rust pada akhir 2021. Apakah langsung bisa? Tentu tidak.

Entah sudah berapa kali saya belajar Rust, dan “belum bisa”. Harus saya akui, belajar Rust ini susah. Pertama, susah karena konsepnya yang lumayan berbeda dengan bahasa pemrograman lainnya. Kedua, bingung hendak membuat project apa.

Tiga tahun berlalu, baru kali ini saya sudah cukup pede untuk menulis program Hello World, serta membuat API yang terkoneksi dengan langsung ke database.

Jadi pesan saya, jika baru pertama kali mencoba Rust, dan kebingungan, maka saya ucapkan: selamat. Kamu di jalan orang yang benar. Yaitu jalannya orang-orang yang bingung.

Menyiapkan Bahan

Ada beberapa pilihan framework untuk membuat backend, Rocket, Actix, dan Axum. Saya telah mencoba semuanya. Dan akhirnya untuk project ini, saya mengikuti petuah @mustafasegf untuk menggunakan Axum.

Selanjutnya, adalah memilih penyambung untuk ke database. Ada beberapa pilihan, Diesel, SeaORM, dan SQLX. Lagi-lagi, karena Bapak @papanberjalan yang sering misuh-misuh soal ORM, saya memutuskan untuk menggunakan SQLX, karena akan bermain dengan raw query SQL.

Mulai Memasak

Dalam tulisan ini, hanya akan meng-highlight beberapa baris kode, dan akan banyak yang simplifikasi. Kode ini ditulis oleh seorang yang baru belajar, sehingga akan banyak kekurangan yang perlu diperbaiki.

Ternyata, struktur untuk membuat backend di Rust tidak jauh berbeda dengan di JavaScript. Mirip dengan Experss.js dan Fastify. Di setiap route, ada path dan handler.

Berikut kodenya.

#[tokio::main]
async fn main() {
    let pool = establish_connection().await.unwrap();
    let app = Router::new()
        .route("/", get(get_all_posts))
        .route("/post/create", post(create_post))
        .with_state(pool);
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

Server akan berjalan di localhost dengan port 3000.

Dari kode di atas, terdapat dua buah route, yaitu “/“ dan “/post/create”. Masing-masing route sudah memiliki handler masing-masing. Artinya, saat sebuah path diakses, maka dia akan menjalankan handler yang tersedia. Misal, jika kita mengakses “/“, maka dia akan menjalankan function get_all_posts.

Axum menyediakan beberapa HTTP method, seperti, GET, POST, dll. Kita tinggal menyesuaikan.

pool adalah koneksi ke database. Saya memasukkannya ke Axum melalui .with_state(pool). State yang berisi pool ini kemudian akan tersedia di setiap handler, sehingga tidak perlu memasukkannya satu-satu. Catatan, saya tidak tahu apakah ini best practice atau bukan. 

Saya baru ingat, akan ada mental model yang sedikit berbeda, saat melakukan development di Rust dan JavaScript. Salah satu yang paling terasa, adalah compile time. Jika di JavaScript wus-wus, maka di Rust terasa (mungkin sangat) lambat.

Hal ini makin berat, dengan tidak adanya “dev” mode. Sehingga saya mengakali dengan crate https://crates.io/crates/cargo-watch. Kemudian menambahkan shortcut di .zshrc/.bashrc:

alias cw="cargo watch -x run"

Shortcut ini akan membuat seperti pnpm dev jika di Next.js. Setiap ada perubahan dalam kode, maka server akan melakukan restart sendiri.

Lanjut, kita akan menilik kode untuk melakukan query ke database.

#[derive(sqlx::FromRow, Serialize)]
pub struct PostSelect {
    pub id: Uuid,
    pub title: String,
    pub content: String,
    pub created_at: Optionchrono::NaiveDateTime,
    pub username: String,
}
pub async fn get_all_posts(State(pool): State<PgPool>) -> Json<Value> {
    let result: Vec<PostSelect> = sqlx::query_as(
        "SELECT 
            p.id, 
            p.title, 
            p.content, 
            p.created_at,
            u.username as username
        FROM posts p
        LEFT JOIN users u ON p.user_id = u.id
        ORDER BY p.created_at DESC
        ",
    )
    .fetch_all(&pool)
    .await
    .expect("Failed to fetch posts");
    Json(json!({"success": true, "data": result}))
}

Sialnya Rust ini adalah, setiap function harus di-define apa output-nya. Berbahagialah bagi yang sudah bermain TypeScript, ini akan terasa lebih ringan.

Saat melakukan return, saya ingin mengembalikan sebuah data berbentuk JSON. Kita bisa melakukan ini dengan men-define -> Json<Value>. Untung saja Json ini adalah sebuah generic. Sehingga kita tidak perlu mendefinisikan secara eksplisit, apa bentuk dari JSON yang akan di-return.

Ingat-ingat, sebelumnya kita telah memasukkan pool ke dalam State yang bisa diakses semua handler. Di function ini, kita mengambil pool tersebut.

Ada beberapa cara untuk melakukan query di SQLX, kita bisa menggunakan query, query!, query_as, query_as!, dan lainnya. Saya sendiri belum mempelajarinya lebih dalam, sejauh ini, yang penting adalah program bisa berjalan.

Ada satu kultur yang terasa berbeda di dunia Rust ini, kita akan dipaksa lebih banyak membaca dokumentasi. Bahkan, tidak jarang membaca source codenya secara langsung. Contoh saja, soal query tadi, kita bisa membacanya di sini docs.rs.

Lagi-lagi, skill membaca dokumentasi ini adalah skill yang sangat penting.

Balik ke kode, saat menulis kode Rust, akan banyak ditemui #[derive()], sebuah syntax yang saya sejujurnya belum tahu bagaimana kerjanya. Tapi yang pasti, dia memiliki semacam sihir untuk mengubah banyak hal.

Contohnya adalah serde::Serialize yang memiliki sihir untuk mengubah struct menjadi object. Tanpa derive itu, struct tidak akan bisa berbentuk object. Dalam hati saya, aneh juga ini.

Terakhir, setelah semua kode berjalan aman dengan cargo run, saatnya lepas landas ke production dengan menjalankan command cargo build --release.

Penutup

Menyenangkan rasanya, setelah tiga tahun belajar Rust, akhirnya bisa merasa lebih nyaman menulis kode dengannya. Meski sebenarnya, saya masih berkelahi dengan borrow checker setiap saat.

Masih banyak hal yang perlu dieksplorasi dan ditingkatkan. Menarik selanjutnya mungkin untuk menulis, bagaimana saya membuat Docker image di GitHub Action lalu menjalankannya di VPS. Perjalanan singkatnya bisa dilihat di tweet ini.

Sebagai tambahan, sekarang, setiap push kode yang saya lakukan ke GitHub, akan langsung di-build dan dijalankan di server.

Terakhir, nasihat saya untuk teman-teman yang memilih jalan untuk mempelajari Rust, gunakan kata “belum bisa” setiap kali stuck dalam belajar, jangan gunakan kata “tidak bisa”. Ini mental model yang terus saya terapkan.

Sebagai buah tangan, saya sering membagikan hal-hal kecil mengenai keseharian programming di akun twitter @zakiego 👋🏻