Daftar Isi
- Apa yang Akan Kita Buat?
- Fitur Mini CMS
- Persiapan
- Langkah 1 — Menambahkan MySQL Crate
- Langkah 2 — Konfigurasi Database
- Struktur Tabel Artikel
- Langkah 3 — Membuat Program Rust
- Memahami Program Utama
- Form Tambah Artikel
- Menyimpan Artikel ke MySQL
- Menampilkan Daftar Artikel
- Mengedit Artikel
- Menghapus Artikel
- Kenapa Menggunakan POST untuk Simpan, Update dan Delete?
- Langkah 4 — Compile Project
- Memastikan Database Dibuat
- Langkah 5 — Membuka CMS dari Browser
- Tampilan Halaman Depan
- Hasil Akhir
- Dari PHP ke Rust
- Kenapa Contoh Ini Menarik?
- Catatan Mengenai Contoh Ini
- Pengembangan Berikutnya
- Kesimpulan
Pada tulisan kedua ini saya ingin melanjutkan percobaan tersebut dengan sesuatu yang sedikit lebih kompleks. Kali ini kita akan membuat sebuah mini CMS sederhana menggunakan Rust dan MySQL.
Kalau pada tutorial sebelumnya halaman HTML langsung ditulis di dalam program Rust, kali ini kita akan mulai berinteraksi dengan database.
Aplikasi yang kita buat akan memiliki halaman depan yang menampilkan daftar artikel, kemudian tersedia tombol untuk menambahkan artikel baru, mengedit artikel, dan menghapus artikel.
Setiap artikel akan memiliki tiga data utama:
- Judul
- Isi artikel
- Kategori
Yang menarik, kita juga akan membuat aplikasi secara otomatis membuat database dan tabel jika belum tersedia. Jadi kita tidak perlu membuat tabel secara manual menggunakan phpMyAdmin atau command line MySQL.
Apa yang Akan Kita Buat?
Hasil akhir dari tutorial ini kurang lebih akan seperti sebuah CMS mini:
Browser
|
v
+---------+
| Nginx |
+---------+
|
v
Rust Application
127.0.0.1:8080
|
v
+---------+
| MySQL |
| db1 |
+---------+
|
v
articles Dari browser kita dapat membuka halaman utama:
https://domain-anda.com/rust Kemudian Rust akan menampilkan daftar artikel dari database MySQL.
Kita juga akan memiliki halaman seperti:
/rust
/rust/add
/rust/edit?id=1 Dengan demikian, tutorial ini mulai mendekati bentuk aplikasi web sederhana yang sebenarnya.
Fitur Mini CMS
Mini CMS yang kita buat memiliki beberapa fitur dasar:
- Menampilkan daftar artikel.
- Menampilkan jumlah artikel.
- Menambahkan artikel.
- Mengedit artikel.
- Menghapus artikel.
- Menyimpan judul artikel.
- Menyimpan isi artikel.
- Menyimpan kategori.
- Menyimpan waktu pembuatan.
- Menyimpan waktu perubahan.
Memang belum bisa disebut CMS lengkap seperti WordPress. Namun untuk belajar Rust, contoh ini sudah cukup menarik karena kita mulai menggabungkan beberapa komponen sekaligus.
Persiapan
Saya menggunakan Alpine Linux yang sebelumnya sudah memiliki Nginx, PHP-FPM, dan MySQL atau MariaDB.
Kita juga melanjutkan project dari tutorial sebelumnya:
hello_world/ Masuk ke directory project:
cd ~/hello_world Langkah 1 — Menambahkan MySQL Crate
Pada tutorial pertama kita belum menggunakan library eksternal. Kali ini kita membutuhkan library untuk berkomunikasi dengan MySQL.
Rust memiliki banyak crate yang dapat digunakan untuk koneksi database. Untuk contoh ini kita menggunakan crate mysql.
Versi yang digunakan pada tutorial ini adalah versi 28:
[dependencies]
mysql = "28" Buka file Cargo.toml:
nano Cargo.toml Kemudian ubah menjadi:
[package]
name = "hello_world"
version = "0.1.0"
edition = "2024"
[dependencies]
mysql = "28" Simpan:
Ctrl + O
Enter
Ctrl + X Cargo nantinya akan mengunduh crate yang diperlukan dan mengompilasinya ketika kita melakukan build.
Crate mysql menyediakan koneksi database, connection pool, query, prepared statement, dan berbagai fasilitas untuk berkomunikasi dengan MySQL maupun server yang kompatibel dengan protokol MySQL.
Langkah 2 — Konfigurasi Database
Untuk tutorial ini kita menggunakan:
Database : db1
User : root
Password : 1
Host : 127.0.0.1
Port : 3306 Aplikasi akan terlebih dahulu melakukan koneksi ke server MySQL menggunakan database sistem mysql.
Kemudian Rust akan menjalankan:
CREATE DATABASE IF NOT EXISTS db1 Setelah database tersedia, aplikasi membuat tabel:
CREATE TABLE IF NOT EXISTS articles Dengan cara ini, saat pertama kali aplikasi dijalankan kita tidak perlu membuat database dan tabel secara manual.
Struktur Tabel Artikel
Tabel yang akan dibuat memiliki struktur seperti berikut:
articles
├── id
├── title
├── content
├── category
├── created_at
└── updated_at Secara SQL kurang lebih:
CREATE TABLE IF NOT EXISTS articles (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
title VARCHAR(255) NOT NULL,
content LONGTEXT NOT NULL,
category VARCHAR(100) NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id)
) Langkah 3 — Membuat Program Rust
Sekarang kita masuk ke file utama:
nano src/main.rs Hapus isi file sebelumnya dan masukkan kode berikut.
use std::io::{Read, Write};
use std::net::TcpListener;
use mysql::prelude::*;
use mysql::{params, Pool};
const DB_HOST: &str = "mysql://root:1@127.0.0.1:3306/mysql";
const DB_APP: &str = "mysql://root:1@127.0.0.1:3306/db1";
fn db_init() -> Result<(), mysql::Error> {
let pool = Pool::new(DB_HOST)?;
let mut conn = pool.get_conn()?;
conn.query_drop(
"CREATE DATABASE IF NOT EXISTS db1
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci"
)?;
drop(conn);
drop(pool);
let pool = Pool::new(DB_APP)?;
let mut conn = pool.get_conn()?;
conn.query_drop(
r"CREATE TABLE IF NOT EXISTS articles (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
title VARCHAR(255) NOT NULL,
content LONGTEXT NOT NULL,
category VARCHAR(100) NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id)
)"
)?;
Ok(())
}
fn db_pool() -> Pool {
Pool::new(DB_APP).expect("Gagal membuat koneksi database")
}
fn html_escape(value: &str) -> String {
value
.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}
fn url_decode(value: &str) -> String {
let value = value.replace('+', " ");
let bytes = value.as_bytes();
let mut result = Vec::new();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'%' && i + 2 < bytes.len() {
let hex = &value[i + 1..i + 3];
if let Ok(number) = u8::from_str_radix(hex, 16) {
result.push(number);
i += 3;
continue;
}
}
result.push(bytes[i]);
i += 1;
}
String::from_utf8_lossy(&result).to_string()
}
fn parse_form(body: &str) -> Vec<(String, String)> {
body.split('&')
.filter_map(|item| {
let mut parts = item.splitn(2, '=');
let key = parts.next()?;
let value = parts.next().unwrap_or("");
Some((
url_decode(key),
url_decode(value),
))
})
.collect()
}
fn form_value(data: &[(String, String)], key: &str) -> String {
data.iter()
.find(|(name, _)| name == key)
.map(|(_, value)| value.clone())
.unwrap_or_default()
}
fn query_value(path: &str, key: &str) -> String {
if let Some(pos) = path.find('?') {
let query = &path[pos + 1..];
for item in query.split('&') {
let mut parts = item.splitn(2, '=');
let name = parts.next().unwrap_or("");
let value = parts.next().unwrap_or("");
if name == key {
return url_decode(value);
}
}
}
String::new()
}
fn layout(title: &str, content: &str) -> String {
format!(
r#"<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{} - Rust CMS</title>
<style>
* {{
box-sizing: border-box;
}}
body {{
margin: 0;
min-height: 100vh;
background:
radial-gradient(circle at top left, #334155 0, transparent 32%),
linear-gradient(135deg, #020617, #0f172a 55%, #1e293b);
font-family: Arial, Helvetica, sans-serif;
color: #334155;
}}
.container {{
width: 94%;
max-width: 1050px;
margin: 0 auto;
}}
header {{
padding: 42px 0 28px;
color: white;
}}
.logo {{
display: inline-flex;
align-items: center;
gap: 12px;
font-size: 24px;
font-weight: bold;
}}
.logo-icon {{
width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 13px;
background: linear-gradient(135deg, #f97316, #ea580c);
box-shadow: 0 8px 25px rgba(249,115,22,.3);
font-size: 25px;
}}
.subtitle {{
margin-top: 10px;
color: #94a3b8;
font-size: 14px;
}}
.panel {{
background: rgba(255,255,255,.97);
border-radius: 22px;
padding: 32px;
margin-bottom: 40px;
box-shadow: 0 25px 60px rgba(0,0,0,.35);
}}
.topbar {{
display: flex;
justify-content: space-between;
align-items: center;
gap: 20px;
margin-bottom: 28px;
}}
h1 {{
margin: 0;
color: #0f172a;
font-size: 30px;
}}
.description {{
margin-top: 7px;
color: #64748b;
}}
.btn {{
display: inline-block;
padding: 11px 18px;
border-radius: 10px;
text-decoration: none;
border: 0;
cursor: pointer;
font-weight: bold;
font-size: 14px;
}}
.btn-primary {{
background: #ea580c;
color: white;
}}
.btn-primary:hover {{
background: #c2410c;
}}
.btn-edit {{
background: #eff6ff;
color: #2563eb;
}}
.btn-delete {{
background: #fef2f2;
color: #dc2626;
}}
.article {{
padding: 22px 0;
border-top: 1px solid #e2e8f0;
}}
.article:first-child {{
border-top: 0;
}}
.article-title {{
margin: 0 0 9px;
color: #0f172a;
font-size: 20px;
}}
.article-content {{
color: #64748b;
line-height: 1.7;
white-space: pre-wrap;
}}
.category {{
display: inline-block;
margin-bottom: 10px;
padding: 5px 10px;
border-radius: 20px;
background: #fff7ed;
border: 1px solid #fed7aa;
color: #c2410c;
font-size: 12px;
font-weight: bold;
}}
.meta {{
margin-top: 12px;
color: #94a3b8;
font-size: 12px;
}}
.actions {{
margin-top: 15px;
display: flex;
gap: 8px;
}}
.empty {{
padding: 55px 20px;
text-align: center;
color: #64748b;
}}
.empty-icon {{
font-size: 50px;
margin-bottom: 15px;
}}
form {{
margin: 0;
}}
label {{
display: block;
margin: 18px 0 7px;
color: #334155;
font-weight: bold;
font-size: 14px;
}}
input,
textarea {{
width: 100%;
padding: 13px 15px;
border: 1px solid #cbd5e1;
border-radius: 10px;
background: #fff;
color: #0f172a;
font: inherit;
outline: none;
}}
input:focus,
textarea:focus {{
border-color: #f97316;
box-shadow: 0 0 0 3px rgba(249,115,22,.12);
}}
textarea {{
min-height: 300px;
resize: vertical;
line-height: 1.6;
}}
.form-actions {{
margin-top: 25px;
display: flex;
gap: 10px;
}}
.btn-secondary {{
background: #e2e8f0;
color: #334155;
}}
.stats {{
display: inline-block;
margin-bottom: 20px;
padding: 7px 12px;
border-radius: 20px;
background: #f0fdf4;
color: #15803d;
font-size: 12px;
font-weight: bold;
}}
footer {{
padding: 0 0 35px;
text-align: center;
color: #64748b;
font-size: 12px;
}}
@media (max-width: 650px) {{
.panel {{
padding: 22px;
}}
.topbar {{
align-items: flex-start;
flex-direction: column;
}}
h1 {{
font-size: 25px;
}}
}}
</style>
</head>
<body>
<div class="container">
🦀
Rust CMS
Mini Content Management System • Rust + MySQL + Alpine Linux
{}
</div> </body> </html>”#, title, content ) } fn page_list(pool: &Pool) -> String { let mut conn = pool.get_conn().expect(“Gagal mengambil koneksi”); let rows: Vec<(u64, String, String, String, String)> = conn .query( “SELECT id, title, content, category, \ DATE_FORMAT(created_at, ‘%d %M %Y %H:%i’) \ FROM articles ORDER BY id DESC” ) .expect(“Gagal mengambil artikel”); let count = rows.len(); let mut articles = String::new(); if rows.is_empty() { articles.push_str( r#”<div class=”empty”> <div class=”empty-icon”>📝</div> <h2>Belum Ada Artikel</h2> <p>Silakan tambahkan artikel pertama menggunakan tombol di atas.</p> </div>”# ); } else { for (id, title, content, category, created) in rows { let safe_title = html_escape(&title); let safe_content = html_escape(&content); let safe_category = html_escape(&category); let safe_created = html_escape(&created); articles.push_str(&format!( r#” <div class=”article”> <div class=”category”>{}</div> <h2 class=”article-title”>{}</h2> <div class=”article-content”>{}</div> <div class=”meta”> Dipublikasikan {} • ID {} </div> <div class=”actions”> <a class=”btn btn-edit” href=”/rust/edit?id={}”> Edit </a> <form method=”POST” action=”/rust/delete” style=”display:inline” onsubmit=”return confirm(‘Hapus artikel ini?’);”> <input type=”hidden” name=”id” value=”{}”> <button class=”btn btn-delete” type=”submit”> Hapus </button> </form> </div> </div> “#, safe_category, safe_title, safe_content, safe_created, id, id, id )); } } let content = format!( r#” <div class=”panel”> <div class=”topbar”> <div> <h1>Daftar Artikel</h1> <div class=”description”> Kelola artikel yang tersimpan di MySQL. </div> </div> <a class=”btn btn-primary” href=”/rust/add”> + Tambah Artikel </a> </div> <div class=”stats”> {} artikel tersimpan </div> {} </div> “#, count, articles ); layout(“Daftar Artikel”, &content) } fn page_form( title: &str, action: &str, article_id: &str, article_title: &str, category: &str, content: &str ) -> String { let safe_title = html_escape(article_title); let safe_category = html_escape(category); let safe_content = html_escape(content); let heading = if article_id.is_empty() { “Tambah Artikel” } else { “Edit Artikel” }; let content = format!( r#” <div class=”panel”> <div class=”topbar”> <div> <h1>{}</h1> <div class=”description”> Isi informasi artikel kemudian simpan ke MySQL. </div> </div> </div> <form method=”POST” action=”{}”> <input type=”hidden” name=”id” value=”{}”> <label for=”title”>Judul Artikel</label> <input type=”text” id=”title” name=”title” value=”{}” placeholder=”Masukkan judul artikel” required > <label for=”category”>Kategori</label> <input type=”text” id=”category” name=”category” value=”{}” placeholder=”Contoh: Linux, Rust, PHP” required > <label for=”content”>Isi Artikel</label> <textarea id=”content” name=”content” placeholder=”Tulis isi artikel di sini…” required >{}</textarea> <div class=”form-actions”> <button class=”btn btn-primary” type=”submit”> Simpan Artikel </button> <a class=”btn btn-secondary” href=”/rust”> Batal </a> </div> </form> </div> “#, heading, action, article_id, safe_title, safe_category, safe_content ); layout(heading, &content) } fn save_article( pool: &Pool, title: &str, content: &str, category: &str ) { let mut conn = pool.get_conn().expect(“Gagal mengambil koneksi”); conn.exec_drop( “INSERT INTO articles (title, content, category) VALUES (:title, :content, :category)”, params! { “title” => title, “content” => content, “category” => category, }, ) .expect(“Gagal menyimpan artikel”); } fn update_article( pool: &Pool, id: &str, title: &str, content: &str, category: &str ) { let id: u64 = id.parse().unwrap_or(0); let mut conn = pool.get_conn().expect(“Gagal mengambil koneksi”); conn.exec_drop( “UPDATE articles SET title = :title, content = :content, category = :category WHERE id = :id”, params! { “id” => id, “title” => title, “content” => content, “category” => category, }, ) .expect(“Gagal memperbarui artikel”); } fn delete_article(pool: &Pool, id: &str) { let id: u64 = id.parse().unwrap_or(0); let mut conn = pool.get_conn().expect(“Gagal mengambil koneksi”); conn.exec_drop( “DELETE FROM articles WHERE id = :id”, params! { “id” => id, }, ) .expect(“Gagal menghapus artikel”); } fn page_edit(pool: &Pool, id: &str) -> String { let id_number: u64 = id.parse().unwrap_or(0); let mut conn = pool.get_conn().expect(“Gagal mengambil koneksi”); let row: Option<(String, String, String)> = conn .exec_first( “SELECT title, content, category FROM articles WHERE id = :id”, params! { “id” => id_number, }, ) .expect(“Gagal mengambil artikel”); match row { Some((title, content, category)) => { page_form( “Edit Artikel”, “/rust/update”, id, &title, &category, &content ) } None => { layout( “Artikel Tidak Ditemukan”, r#” <div class=”panel”> <h1>Artikel Tidak Ditemukan</h1> <p>Artikel yang diminta tidak tersedia.</p> <br> <a class=”btn btn-primary” href=”/rust”> Kembali </a> </div> “# ) } } } fn redirect(location: &str) -> String { format!( “HTTP/1.1 302 Found\r\n\ Location: {}\r\n\ Content-Length: 0\r\n\ Connection: close\r\n\ \r\n”, location ) } fn response_html(html: &str) -> String { format!( “HTTP/1.1 200 OK\r\n\ Content-Type: text/html; charset=UTF-8\r\n\ Content-Length: {}\r\n\ Connection: close\r\n\ \r\n\ {}”, html.as_bytes().len(), html ) } fn main() { db_init().expect(“Gagal menyiapkan database”); let pool = db_pool(); println!(“Database db1 siap digunakan.”); println!(“Rust CMS berjalan di http://127.0.0.1:8080”); let listener = TcpListener::bind(“127.0.0.1:8080”) .expect(“Gagal menjalankan server”); for stream in listener.incoming() { let mut stream = match stream { Ok(stream) => stream, Err(_) => continue, }; let mut buffer = [0u8; 65536]; let size = match stream.read(&mut buffer) { Ok(size) => size, Err(_) => continue, }; let request = String::from_utf8_lossy(&buffer[..size]); let mut request_parts = request.split(“\r\n\r\n”); let headers = request_parts.next().unwrap_or(“”); let body = request_parts.next().unwrap_or(“”); let request_line = headers.lines().next().unwrap_or(“”); let mut parts = request_line.split_whitespace(); let method = parts.next().unwrap_or(“”); let path = parts.next().unwrap_or(“/”); let response = match (method, path) { (“GET”, “/rust”) | (“GET”, “/rust/”) => { response_html(&page_list(&pool)) } (“GET”, “/rust/add”) => { response_html( &page_form( “Tambah Artikel”, “/rust/save”, “”, “”, “”, “” ) ) } (“GET”, path) if path.starts_with(“/rust/edit”) => { let id = query_value(path, “id”); response_html(&page_edit(&pool, &id)) } (“POST”, “/rust/save”) => { let data = parse_form(body); let title = form_value(&data, “title”); let category = form_value(&data, “category”); let content = form_value(&data, “content”); save_article( &pool, &title, &content, &category ); redirect(“/rust”) } (“POST”, “/rust/update”) => { let data = parse_form(body); let id = form_value(&data, “id”); let title = form_value(&data, “title”); let category = form_value(&data, “category”); let content = form_value(&data, “content”); update_article( &pool, &id, &title, &content, &category ); redirect(“/rust”) } (“POST”, “/rust/delete”) => { let data = parse_form(body); let id = form_value(&data, “id”); delete_article(&pool, &id); redirect(“/rust”) } _ => { response_html( &layout( “404”, r#” <div class=”panel”> <h1>404</h1> <p>Halaman tidak ditemukan.</p> <br> <a class=”btn btn-primary” href=”/rust”> Kembali ke Artikel </a> </div> “# ) ) } }; let _ = stream.write_all(response.as_bytes()); } }
Memahami Program Utama
Kode di atas memang jauh lebih panjang dibandingkan tutorial pertama. Jangan khawatir jika pada tahap awal terlihat rumit.
Kita sebenarnya sedang menggabungkan beberapa konsep sederhana menjadi satu aplikasi.
1. Koneksi ke MySQL
const DB_HOST: &str =
"mysql://root:1@127.0.0.1:3306/mysql";
const DB_APP: &str =
"mysql://root:1@127.0.0.1:3306/db1"; Variabel pertama digunakan ketika aplikasi perlu membuat database db1.
Setelah database tersedia, aplikasi menggunakan koneksi kedua untuk bekerja dengan database tersebut.
2. Membuat Database Otomatis
CREATE DATABASE IF NOT EXISTS db1 Penggunaan IF NOT EXISTS membuat query tersebut aman dijalankan berulang kali.
Jika database sudah ada, MySQL tidak akan membuat database baru.
3. Membuat Tabel Otomatis
CREATE TABLE IF NOT EXISTS articles (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
title VARCHAR(255) NOT NULL,
content LONGTEXT NOT NULL,
category VARCHAR(100) NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id)
) Hal ini membuat tutorial lebih praktis. Kita tidak perlu membuka phpMyAdmin atau masuk ke command line MySQL hanya untuk membuat tabel.
Form Tambah Artikel
Ketika kita membuka:
/rust/add Rust akan menampilkan form yang terdiri dari:
- Judul artikel.
- Kategori.
- Isi artikel.
- Tombol Simpan Artikel.
- Tombol Batal.
Form tersebut menggunakan HTTP POST:
<form method="POST" action="/rust/save"> Ketika tombol simpan ditekan, browser mengirim data menuju endpoint:
/rust/save Kemudian Rust membaca data tersebut dan memasukkannya ke MySQL.
Menyimpan Artikel ke MySQL
Bagian yang melakukan penyimpanan adalah:
conn.exec_drop(
"INSERT INTO articles (title, content, category)
VALUES (:title, :content, :category)",
params! {
"title" => title,
"content" => content,
"category" => category,
},
)?; Di sini kita menggunakan parameter query daripada menyusun SQL menggunakan string secara langsung.
Pendekatan tersebut penting karena data yang berasal dari form tidak sebaiknya langsung ditempelkan ke query SQL.
Menampilkan Daftar Artikel
Ketika membuka:
/rust Rust menjalankan query:
SELECT id, title, content, category, created_at
FROM articles
ORDER BY id DESC Artikel terbaru akan muncul paling atas.
Setiap artikel kemudian dibuat menjadi HTML secara dinamis.
Jadi sekarang HTML yang kita lihat di browser tidak lagi semuanya ditulis secara statis. Sebagian data berasal dari MySQL.
Mengedit Artikel
Setiap artikel memiliki tombol Edit.
Misalnya artikel memiliki ID:
5 Ketika tombol Edit diklik, browser menuju:
/rust/edit?id=5 Rust mengambil ID tersebut dan menjalankan query:
SELECT title, content, category
FROM articles
WHERE id = :id Data dari database kemudian dimasukkan kembali ke form.
Setelah tombol Simpan Artikel ditekan, request dikirim ke:
/rust/update Kemudian Rust menjalankan:
UPDATE articles
SET title = :title,
content = :content,
category = :category
WHERE id = :id Menghapus Artikel
Kita juga menyediakan tombol Hapus.
Untuk menghindari penghapusan secara tidak sengaja, browser akan menampilkan konfirmasi terlebih dahulu.
onsubmit="return confirm('Hapus artikel ini?');" Jika pengguna menyetujui, browser mengirim POST ke:
/rust/delete Kemudian Rust menjalankan:
DELETE FROM articles
WHERE id = :id Setelah proses selesai, pengguna diarahkan kembali ke halaman daftar artikel.
Kenapa Menggunakan POST untuk Simpan, Update dan Delete?
Pada contoh ini kita membedakan request GET dan POST.
| Method | Fungsi |
|---|---|
| GET | Menampilkan halaman atau mengambil data |
| POST | Mengirim perubahan data |
Contohnya:
GET /rust
GET /rust/add
GET /rust/edit?id=1
POST /rust/save
POST /rust/update
POST /rust/delete Pemisahan seperti ini akan semakin penting ketika aplikasi mulai berkembang menjadi aplikasi web yang lebih besar.
Langkah 4 — Compile Project
Setelah kode selesai dimasukkan, kita dapat memeriksa terlebih dahulu apakah kode Rust dapat dikompilasi:
cargo check Jika tidak ada error, lanjutkan dengan:
cargo run Pada build pertama, Cargo mungkin membutuhkan waktu lebih lama karena harus mengunduh dan mengompilasi dependency MySQL.
Jika berhasil, kira-kira akan muncul:
Database db1 siap digunakan.
Rust CMS berjalan di http://127.0.0.1:8080 Memastikan Database Dibuat
Sekarang kita dapat masuk ke MySQL atau MariaDB untuk memastikan database sudah dibuat.
mysql -u root -p Kemudian masukkan password:
1 Lalu:
SHOW DATABASES; Seharusnya terdapat:
db1 Kemudian:
USE db1;
SHOW TABLES; Seharusnya terdapat:
articles Kita juga dapat melihat strukturnya:
DESCRIBE articles; Langkah 5 — Membuka CMS dari Browser
Jika Rust server masih berjalan di:
127.0.0.1:8080 kita dapat mengujinya terlebih dahulu:
curl http://127.0.0.1:8080/rust Namun jika menggunakan Nginx seperti tutorial sebelumnya, kita dapat membuat reverse proxy:
location /rust {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 12000;
} Kemudian cek konfigurasi:
nginx -t Jika tidak ada error:
nginx -s reload Sekarang buka:
https://domain-anda.com/rust Tampilan Halaman Depan
Jika database masih kosong, kita akan mendapatkan tampilan seperti halaman CMS dengan pesan bahwa belum ada artikel.
Klik:
+ Tambah Artikel Kemudian akan muncul form dengan desain yang lebih modern.
Isi misalnya:
Judul:
Belajar Rust Terhubung MySQL
Kategori:
Rust
Isi:
Hari ini saya mencoba membuat aplikasi web
sederhana menggunakan Rust dan MySQL. Kemudian klik:
Simpan Artikel Setelah berhasil disimpan, kita akan kembali ke halaman depan dan artikel tersebut langsung muncul dari database.
Hasil Akhir
Sekarang kita sudah memiliki aplikasi yang alurnya kurang lebih seperti berikut:
Browser
|
v
Nginx
|
v
Rust Application
|
+------------+------------+
| | |
v v v
List Add Edit
| | |
+------------+------------+
|
v
MySQL
|
v
db1
|
v
articles Pada tahap ini Rust sudah tidak hanya menghasilkan halaman HTML statis.
Aplikasi sudah dapat:
- Menerima request HTTP.
- Membaca parameter URL.
- Membaca data dari form.
- Menghubungkan aplikasi dengan MySQL.
- Membuat database.
- Membuat tabel.
- Membaca data.
- Menambahkan data.
- Mengubah data.
- Menghapus data.
- Menghasilkan HTML secara dinamis.
Dari PHP ke Rust
Kalau terbiasa dengan PHP, konsep aplikasi ini sebenarnya cukup mudah dibandingkan jika kita melihat kode Rust-nya satu per satu.
Misalnya pada PHP kita mungkin membuat:
$query = "SELECT * FROM articles";
$result = mysqli_query($conn, $query); Sedangkan pada Rust kita menggunakan crate MySQL:
let rows: Vec<(u64, String, String, String, String)> =
conn.query(
"SELECT id, title, content, category, created_at
FROM articles
ORDER BY id DESC"
)?; Konsepnya sama-sama mengambil data dari database. Perbedaannya adalah Rust memiliki sistem type yang lebih ketat dan compiler akan membantu menemukan banyak kesalahan sebelum program dijalankan.
Kenapa Contoh Ini Menarik?
Menurut saya bagian yang paling menarik dari percobaan ini adalah kita mulai melihat Rust dari sisi aplikasi web, bukan hanya sebagai bahasa untuk membuat program command line.
Pada tutorial pertama kita hanya memiliki:
Rust
|
+-- TCP Listener
|
+-- HTML Response Sekarang sudah berkembang menjadi:
Rust
|
+-- HTTP Request
|
+-- Routing sederhana
|
+-- Form
|
+-- HTML
|
+-- MySQL
|
+-- CRUD
|
+-- Nginx Reverse Proxy Ini sudah mulai terasa seperti sebuah aplikasi web sungguhan.
Catatan Mengenai Contoh Ini
Walaupun aplikasi ini sudah memiliki fitur CRUD, program tersebut masih merupakan contoh pembelajaran dan belum cocok langsung digunakan sebagai CMS production.
Masih banyak hal yang perlu diperbaiki, misalnya:
- Authentication dan login administrator.
- CSRF protection.
- Validasi input yang lebih lengkap.
- Session management.
- Pagination.
- Routing yang lebih baik.
- Template engine.
- Logging.
- Error handling yang lebih baik.
- Connection management yang lebih matang.
- Graceful shutdown.
- HTTPS melalui Nginx.
- Konfigurasi database melalui environment variable.
Khusus username dan password database, pada contoh ini kita sengaja menuliskannya langsung di source code agar lebih mudah dipahami.
Untuk aplikasi production, sebaiknya password tidak ditulis seperti:
mysql://root:1@127.0.0.1:3306/db1 Sebaiknya menggunakan user database khusus aplikasi dan menyimpan credential melalui environment variable atau mekanisme konfigurasi yang lebih aman.
Pengembangan Berikutnya
Dari mini CMS ini kita sebenarnya sudah memiliki dasar yang cukup untuk melanjutkan pembelajaran Rust web development.
Pada tahap berikutnya, aplikasi dapat dikembangkan menjadi:
- CMS dengan login administrator.
- Editor artikel yang lebih lengkap.
- Upload gambar.
- URL artikel berdasarkan slug.
- Kategori dari tabel tersendiri.
- Pencarian artikel.
- Pagination.
- REST API.
- JSON response.
- Frontend JavaScript.
- Framework web Rust seperti Axum atau Actix Web.
Kesimpulan
Pada tutorial kedua ini kita sudah membuat sesuatu yang lebih kompleks dibandingkan web server pada tutorial sebelumnya.
Kita tidak lagi hanya mengirim HTML dari Rust. Sekarang aplikasi sudah dapat berkomunikasi dengan MySQL, membuat database dan tabel secara otomatis, menampilkan artikel, menerima form, menyimpan artikel, mengedit artikel, serta menghapus artikel.
Struktur sederhananya menjadi:
Browser
|
v
Nginx
|
v
Rust
|
v
MySQL
|
v
db1 / articles Bagi saya, pendekatan seperti ini cukup menarik untuk belajar Rust karena kita dapat melihat perkembangan aplikasi secara bertahap.
Pada tutorial pertama kita belajar bagaimana Rust membuat web server sederhana. Pada tutorial kedua ini kita mulai menghubungkan Rust dengan database dan membuat CRUD.
Langkah berikutnya akan menjadi lebih menarik karena kita dapat mulai meninggalkan HTTP server buatan sendiri dan menggunakan framework Rust yang memang dirancang untuk aplikasi web, kemudian membandingkan cara membuat aplikasi yang sama dengan pendekatan framework.