Senin, 02 September 2024

Membuat restful api sederhana dengan framework actix web dan database mysql

 Sebelum kita mulai koding alangkah baiknya kita baca basmallah dalam hati.

Yuk kita intip langkah - langkah apa saja yang perlu disiapkan dalam membuat restful api ini.

- Pertama kalian harus install rust di dalam komputer masing - masing buka linknya di sini https://www.rust-lang.org/tools/install

- Kedua setelah selesai penginstalan kita mulai pembuatan projeknya ya, ketikkan perintah di dalam terminal. " cargo new nama projek"

- Ketiga siapkan database mysql dengan nama blog_db dan  buat tabel baru di dalam database tersebut misalnya posts. Isi tabel tersebut ada id, title dan content

- Keempat , langkah ini barulah kita buatkan script atau kodenya. Di file main.rs isi kodenya seperti ini ya:

mod handler;
mod model;
mod schema;




use actix_web::middleware::Logger;
use actix_web::{web, App, HttpServer};
use sqlx::mysql::{MySqlPool, MySqlPoolOptions};
use dotenv::dotenv;

pub struct AppState {
    db: MySqlPool,
}


#[actix_web::main]
async fn main() -> std::io::Result<()>{
    if std::env::var_os("RUST_LOG").is_none() {
        std::env::set_var("RUST_LOG", "actix_web=info")
    }
    dotenv().ok();
    env_logger::init();

    let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
    let pool = match MySqlPoolOptions::new()
            .max_connections(10)
            .connect(&database_url)
            .await
    {
        Ok(pool) => {
            println!("✅Connection to the database is successful!");
            pool
        },
        Err(err) => {
            println!("🔥 Failed to connect to the database: {:?}", err);
            std::process::exit(1);
        }  
    };

    println!("🚀 Server started successfully");

    HttpServer::new(move || {
        App::new()
        .app_data(web::Data::new(AppState { db : pool.clone()}))
        .wrap(Logger::default())
        .service(handler::get_post)
        .service(handler::create_post)
        .service(handler::get_single_post)
        .service(handler::edit_post)
        .service(handler::delete_post)
       
    })
    .bind(("127.0.0.1", 5050))?
            .run()
            .await

}

selain file main.rs ada beberapa file yang perlu dipersiapkan diantaranya: handler.rs, model.rs dan schema.rs. 

Di dalam file handler.rs memiliki tugas untuk menjalankan segala service yang dibutuhkan dalam mengolah data untuk kodenya bisa dilihat di bawah ini.

use actix_web::{delete, get, patch, post, web, HttpResponse, Responder};
use serde_json::json;
use crate::{
    model::{Post, PostResponse}, schema::{CreatePostSchema, FilterOptions}, AppState
};


fn filter_db_record(post: &Post) -> PostResponse {
    PostResponse {
        id: post.id.to_owned(),
        title: post.title.to_owned(),
        content: post.content.to_owned(),
    }
}

#[get("/api/posts")]
pub async fn get_post(opt: web::Query<FilterOptions>,config: web::Data<AppState>)-> impl Responder{
    let limit = opt.limit.unwrap_or(10);
    let offset = (opt.page.unwrap_or(1) - 1) * limit;
    let posts = sqlx::query_as!(Post, r#"SELECT id, title, content FROM posts ORDER BY id DESC LIMIT ? OFFSET ?"#, limit as i32, offset as i32)
        .fetch_all(&config.db)
        .await
        .unwrap();

    let post_resp = posts
                            .into_iter()
                            .map(|post| filter_db_record(&post))
                            .collect::<Vec<PostResponse>>();
    let response = serde_json::json!({
        "status" : "success",
        "posts" : post_resp
    });

    HttpResponse::Ok().json(response)
}

#[post("/api/posts")]
pub async fn create_post(body: web::Json<CreatePostSchema>, config: web::Data<AppState>) -> impl Responder{
    sqlx::query!(r#"INSERT INTO posts(title, content) VALUES(?, ?)"#, body.title, body.content)
        .execute(&config.db)
        .await;

    let query_result = sqlx::query_as!(Post ,r#"SELECT id, title, content FROM posts WHERE title =?"#, body.title)
        .fetch_one(&config.db)
        .await;

    match query_result {
        Ok(value) => {
            let post_response = serde_json::json!({"status": "success", "data": serde_json::json!({"post": filter_db_record(&value)})});

            return HttpResponse::Ok().json(post_response);
        },
        Err(err) => {
            return HttpResponse::InternalServerError().json(serde_json::json!({
                "status": "error",
                "message": format!("{:?}", err)
            }));
        }
    }
}

#[get("/api/posts/{id}")]
pub async fn get_single_post(path: web::Path<i32>, config: web::Data<AppState>) -> impl Responder{
    let post_id = path.into_inner().to_string();
    let query_result = sqlx::query_as!(Post, r#"SELECT id, title, content FROM posts WHERE id=?"#, post_id)
                    .fetch_one(&config.db)
                    .await;

    match query_result {
        Ok(post) => {
            let post_response = serde_json::json!({"status": "success", "data": serde_json::json!({"post": filter_db_record(&post)})});

            return HttpResponse::Ok().json(post_response)
        },
        Err(err) => {
            return HttpResponse::InternalServerError()
                    .json(serde_json::json!({"status": "error", "message": format!("{:?}", err)}));
        }
    }
}

#[patch("/api/posts/{id}")]
pub async fn edit_post(path: web::Path<i32>, body: web::Json<CreatePostSchema>, config: web::Data<AppState>)-> impl Responder{
    let post_id = path.into_inner().to_string();

    let update_result = sqlx::query(r#"UPDATE posts SET title = ?, content = ? WHERE id= ?"#)
    .bind(body.title.to_owned())
    .bind(body.content.to_owned())
    .bind(post_id.to_owned())
    .execute(&config.db)
    .await;
   
    match  update_result{
        Ok(value) => {
            if value.rows_affected() == 0 {
                let message = format!("Post with ID: {} not found", post_id);
                return HttpResponse::NotFound().json(json!({"status": "fail", "message": message}));
            }
        },
        Err(err) => {
            let message = format!("Internal server error: {}", err);
            return HttpResponse::InternalServerError().json(json!({"status": "error", "message": message}));
        }
    }

    let _result = sqlx::query_as!(Post, r#"SELECT id, title, content FROM posts WHERE id=?"#, post_id.to_owned())
            .fetch_one(&config.db)
            .await;
    match _result {
        Ok(row) => {
            let note_response = serde_json::json!({"status": "success","data": serde_json::json!({
                "note": filter_db_record(&row)
            })});

            HttpResponse::Ok().json(note_response)
        }
        Err(e) => HttpResponse::InternalServerError()
            .json(serde_json::json!({"status": "error","message": format!("{:?}", e)})),
    }
}

#[delete("/api/posts/{id}")]
pub async fn delete_post(path: web::Path<i32>, config: web::Data<AppState>) -> impl Responder{
    let post_id = path.into_inner().to_string();
    let query_result = sqlx::query!(r#"DELETE FROM posts WHERE id = ?"#, post_id )
                .execute(&config.db).await;

    match  query_result {
        Ok(result) => {
            if result.rows_affected() == 0 {
                let message = format!("Post with ID: {} not found", post_id);
                return HttpResponse::NotFound().json(json!({"status": "fail", "message": message}));
            } else {
                return HttpResponse::Ok().json(json!({"status": "success", "message": "Delete record successfully"}));
            }
        },
        Err(err) => {
            return HttpResponse::InternalServerError().json(json!({"status": "error", "message": format!("Internal server error {:?}", err)}));
        }
    }
}

Selanjutnya di dalam file model berisi field yang dibutuhkan di dalam tabel posts:

use serde::{Deserialize, Serialize};

#[derive(Debug, sqlx::FromRow, Serialize, Deserialize)]
#[allow(non_snake_case)]
pub struct  Post {
    pub id: i32,
    pub title: Option<String>,
    pub content: Option<String>,
}

#[derive(Debug, Serialize, Deserialize)]
#[allow(non_snake_case)]
pub struct  PostResponse {
    pub id: i32,
    pub title: Option<String>,
    pub content: Option<String>,
}

Kemudian file schema.rs memiliki beberapa parameter atau inputan query yang digunakan di setiap endpointnya

use serde::{Deserialize, Serialize};

#[derive(Deserialize, Debug)]
pub struct FilterOptions {
    pub page: Option<usize>,
    pub limit: Option<usize>,
}

#[derive(Deserialize, Debug)]
pub struct ParamOptions {
    pub id: String,
}

#[derive(Serialize, Deserialize, Debug)]
pub struct CreatePostSchema{
    pub title: Option<String>,
    pub content: Option<String>,
}

ada 2 file yang gak boleh kelewat ya .env dan cargo.toml . masing - masing dari file tersebut memiliki tugas yang berbeda - beda. Jika file .env bertugas untuk konfigurasi koneksi database mysql sedangkan cargo.toml berisi modul - modul program yang  yang dibutuhkan dalam pembuatan projek tersebut.

Settingan dari koneksi database mysql di dalam file .env sangat mudah sekali yaitu:

MYSQL_USER= root
MYSQL_DATABASE=blog_db
MYSQL_ROOT_PASSWORD=password
MYSQL_PASSWORD=password
DATABASE_URL=mysql://${MYSQL_USER}:${MYSQL_ROOT_PASSWORD}@localhost:3306/${MYSQL_DATABASE}

Tambahkan modul atau library di file cargo.toml ya

[dependencies]
sqlx = { version = "0.8", features = ["mysql", "runtime-async-std"]}
actix-web = "4.9.0"
mysql = "25.0.1"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
chrono = { version = "0.4", features = ["serde"] }
derive_more = "0.99"
env_logger = "0.11"
isahc = "1.7"
log="0.4"
dotenv = "0.15.0"

Jika semua kode sudah dikerjakan dengan benar di dalam projek anda maka anda bisa melakukan uji coba api lewat curl saja.
1. Method Post : Create new a record
    curl -H 'Content-Type: application/json' \
      -d '{ "title": "lorem ipsum", "content" : "lorem ipsum dolar sit ae"}' \
      -X POST \
      http://127.0.0.1:5050/api/posts
2. Method Get : singgle record
    curl -H 'Content-Type: application/json' \
      -X GET  http://127.0.0.1:5050/api/posts/6
3. Method Get : All records
    curl -H 'Content-Type: application/json' \
    -X GET http://127.0.0.1:5050/api/posts
4. Method Patch : update record by id
    curl -H 'Content-Type: application/json' \
      -d '{ "title": "Hello coders", "content" : "Hello coders, how are you?"}' \
      -X PATCH \
      http://127.0.0.1:5050/api/posts/6
5. Method Delete : delete record
    curl -H 'Content-Type: application/json' \
      -X DELETE http://127.0.0.1:5050/api/posts/6

Begitulah tahapan demi tahapan dalam pembuatan simpel restful api menggunakan bahasa pemrograman rust alias framework actix web. Semoga pembelajaran ini bisa bermanfaat di kemudian hari. Selamat mencobanya ya....heeppiii coding brooo.


0 komentar:

Posting Komentar

Kastemisasi tampilan terminal linux menjadi keren

Install Oh My Posh langkah pertama anda dapat menginstal Oh My Posh dan Unzip agar dapat mengekstraknya, ketikkan perintah di terminal. sudo...