如何使用 Actix Web 建立 REST API
技術文章

如何使用 Actix Web 建立 REST API

#rust #api #Actix Web
如何使用 Actix Web 建立 REST API

五倍技術部

在當今的網路開發中,REST API 已成為許多應用程式和系統之間交互的主要方式。Actix Web,作為一個高效能的 Rust 網頁框架,提供了建立這些 API 的強大工具。

本文章將會帶你走過使用 Actix Web 建立 REST API 的整個過程,從專案的初始化到實作 Todo List 的 CRUD API。

建立新專案

開始新增一個專案吧!專案名稱這裡就用 actix_todo,在終端機執行:

cargo new actix_todo

建立完專案後,接下來這個專案還需要安裝以下幾個套件:

  • actix-web: Actix Web 是一個高效能、非常靈活的 Rust 網頁框架。可以建立網頁伺服器、客戶端。

  • serde: Serde 是一個 Rust 的序列化框架,允許將複雜的數據結構轉換成為像是 JSON 這樣的簡單格式,並且還能從這些格式中反序列化回 Rust 的數據結構。在許多 REST API 的實作中,Serde 是不可或缺的工具,因為它使得數據的交換變得非常簡單。

  • chrono: Chrono 是一個處理日期和時間的 Rust 函式庫。它提供了日期、時間、日期時間、時區等功能,非常適合在需要時間戳記或日期操作的情境中使用。

  • uuid: UUID 函式庫允許生成和解析唯一的通用識別碼 (UUID)。在許多系統中,UUIDs 常被用作唯一識別資源或物件,特別是當傳統的自動遞增 ID 不適用或不夠安全時。

安裝方式只要在終端機分別執行以下指令:

cargo add actix-web
cargo add serde --features derive
cargo add chrono --features serde
cargo add uuid --features v4

建立第一支

1. 引入所需的模組和函式庫

use actix_web::{get, web, App, HttpResponse, HttpServer, Responder, Result};
use serde::{Serialize};

第一步要先引入 Actix Web 框架中所需的各種元件,以及 Serde 的序列化功能。

2. 定義回應結構

#[derive(Serialize)]
pub struct Response {
    pub message: String,
}

這裡我們定義了一個名為 Response 的結構,它包含一個公開的 message,型別為 String。#[derive(Serialize)] 是一個標記,它讓 Response 結構可以被自動序列化為 JSON 格式。

3. 定義健康檢查路由

#[get("/health")]
async fn healthcheck() -> impl Responder {
    let response = Response {
        message: "Everything is working fine".to_string(),
    };
    HttpResponse::Ok().json(response)
}

這段程式碼定義了一個非同步的 healthcheck 函式,這個函式會回應一個表示 API 運作正常的訊息。#[get("/health")] 是一個路由標記,當使用者訪問 /health 路徑時,這個函式會被呼叫。函式中,我們建立了一個 Response 物件並返回了一個 HTTP 200 OK 的回應,包含該訊息。

4. 定義未找到的路由

async fn not_found() -> Result<HttpResponse> {
    let response = Response {
        message: "Resource not found".to_string(),
    };
    Ok(HttpResponse::NotFound().json(response))
}

當其他路由不符合的時候,這個函式將被呼叫。它回應一個 HTTP 404 Not Found 的回應,包含一個表示資源未找到的訊息。

5. 主函式

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| App::new().service(healthcheck).default_service(web::route().to(not_found)))
        .bind(("127.0.0.1", 8080))?
        .run()
        .await
}

最後在整個程式的入口點 - main。#[actix_web::main] 是一個特殊的標記,使得非同步函式可以作為主函式使用。在 main 中,我們建立了一個 HTTP 伺服器,指定了 /health 路由到 healthcheck(),並設定了預設路由到 not_found()
最後,伺服器被綁定到本地的 127.0.0.1,端口 8080,並啟動它。

在終端機執行:

cargo run

在 Postman 測試

模組化開發

隨著我們進一步深入 Actix Web 和 Rust,我們將會遇到越來越多的程式碼。在這種情況下,簡單地將所有功能和結構都放在一個檔案中可能會讓事情變得混亂。

在 Rust 中,模組系統允許我們將代碼分割成小的、可管理的單位,每個單位都具有明確的職責。這不僅使我們的應用程式結構更加清晰,還可以更容易地重複使用和測試特定的功能模組。

在接下來的部分,我們會示範如何使用 Rust 的模組系統來組織 Actix Web 專案,並且會繼續新增其他 API。

在開始模組化開發之前,我們要先新增幾個資料夾跟檔案。

mkdir src/api src/models src/repository
touch src/api/mod.rs src/models/mod.rs src/repository/mod.rs

main.rs 引入 mod:

use actix_web::{get, web, App, HttpResponse, HttpServer, Responder, Result};
use serde::Serialize;

mod api;
mod models;
mod repository;

#[derive(Serialize)]
pub struct Response {
    pub message: String,
}

開始 Todo List 的 API Endpoint

A. 建立 Todo 的資料 Model

我們在 src/models 目錄下建立一個新的 todo.rs,要建立 Todo 的資料 Model,代表一個待辦事項。會用到 chronoserde,分別處理時間還有 JSON 格式。

// src/models/todo.rs
use chrono::prelude::{DateTime, Utc};
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Todo {
    pub id: Option<String>,
    pub title: String,
    pub description: Option<String>,
    pub created_at: Option<DateTime<Utc>>,
    pub updated_at: Option<DateTime<Utc>>,
}

接著在 src/models/mod.rs 做一個模組聲明:

pub mod todo;

B. 定義一個內存資料庫來存儲 Todo 物件

由於本文章的範例沒有使用像是 PostgreSQL 之類的資料庫來做存取,比較著重在 REST API 的示範,所以我們會自己定義一個簡單的內存資料庫來示範。

src/repository 目錄下建立一個新的 database.rs

1. 引入所需的模組和函式庫

use std::fmt::Error;
use chrono::prelude::*;
use std::sync::{Arc, Mutex};
use crate::models::todo::Todo;

這些 use 聲明引入了:

  • 錯誤格式化功能。
  • 日期和時間相關的功能。
  • Arc 和 Mutex,這是 Rust 中的同步原語,用於多線程共享和修改資料。
  • 從 models 模組引入的 Todo 結構。

2. 定義資料庫結構

pub struct Database {
    pub todos: Arc<Mutex<Vec<Todo>>>,
}

Database 結構具有一個公開的字段 todos,它是一個受 Mutex 保護的 Todo 物件的向量,這表示可以在多線程環境中安全地修改它。

3. 為資料庫結構實現功能

impl Database {
    pub fn new() -> Self {
        let todos = Arc::new(Mutex::new(vec![]));
        Database { todos }
    }

    pub fn create_todo(&self, todo: Todo) -> Result<Todo, Error> {
        let mut todos = self.todos.lock().unwrap();
        let id = uuid::Uuid::new_v4().to_string();
        let created_at = Utc::now();
        let updated_at = Utc::now();
        let todo = Todo {
            id: Some(id),
            created_at: Some(created_at),
            updated_at: Some(updated_at),
            ..todo
        };
        todos.push(todo.clone());
        Ok(todo)
    }
}

這個 impl 區塊為 Database 結構增加了功能和方法。

  • new 方法創建一個新的 Database 實例,其中 todos 是一個空向量。

  • create_todo 方法將一個新的 Todo 物件加入到 todos 向量中。該方法生成一個新的 UUID 作為 ID,設置當前的 UTC 日期和時間作為新增和更新時間,然後將 Todo 物件加入到向量中。

4. 在 src/repository/mod.rs 中的模組聲明

pub mod database;

C. 定義與 Todo 相關的 API 端點

src/api 目錄下建立一個新的 api.rs

1. 引入所需的模組和函式庫

use actix_web::web;
use actix_web::{web::{Data, Json}, post, HttpResponse};
use crate::{models::todo::Todo, repository::database::Database};

這些 use 主要引入了:

  • Actix Web 的核心組件,用於建立 Web 應用。
  • 用於資料提取和反序列化 JSON 數據的功能。
  • 從專案中引入的 Todo 結構和 Database 模組。

2. 定義 API Endpoint

#[post("/todos")]
pub async fn create_todo(db: Data<Database>, new_todo: Json<Todo>) -> HttpResponse {
    let todo = db.create_todo(new_todo.into_inner());
    match todo {
        Ok(todo) => HttpResponse::Ok().json(todo),
        Err(err) => HttpResponse::InternalServerError().body(err.to_string()),
    }
}

這是一個異步的函式,用於新增一個新的待辦事項。它期望從 HTTP POST 請求的正文中接收一個 JSON 格式的 Todo 物件。主要接受兩個參數:
- Database 的參考。
- 客戶端提供的新的 Todo 物件。

會建立新的待辦事項並返回適當的 HTTP 響應。

3. 配置 API

pub fn config(cfg: &mut web::ServiceConfig) {
    cfg.service(
        web::scope("/api")
            .service(create_todo)
    );
}

這個 config() 接受一個 ServiceConfig 的參考,並將 API Endpoint 加入它。在此例中,它將 create_todo() 設置為在 /api/todos 路徑上響應。

4. 在 src/api/mod.rs 中的模組聲明

pub mod api;

這行程式碼將 src/api/api.rs 文件作為一個公開的子模組 api 引入到 src/api 模組中。這樣,其他 Rust 代碼可以訪問和使用在 api 子模組中定義的所有公開功能。

最後則是在 main.rs

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    let todo_db = repository::database::Database::new();
    let app_data = web::Data::new(todo_db);

    HttpServer::new(move ||
        App::new()
            .app_data(app_data.clone())
            .configure(api::api::config)
            .service(healthcheck)
            .default_service(web::route().to(not_found))
            .wrap(actix_web::middleware::Logger::default())
    )
        .bind(("127.0.0.1", 8080))?
        .run()
        .await
}

然後執行 cargo run,就可以進行測試了。

在 Postman 測試新增 todo

繼續新增其他的 Endpoint。

GET /todos

src/api/api.rs 新增函式:

use actix_web::{web::{
    Data,
    Json,
}, post, get, HttpResponse};

// 省略

#[get("/todos")]
pub async fn get_todos(db: web::Data<Database>) -> HttpResponse {
    let todos = db.get_todos();
    HttpResponse::Ok().json(todos)
}

pub fn config(cfg: &mut web::ServiceConfig) {
    cfg.service(
        web::scope("/api")
            .service(create_todo)
            .service(get_todos)
    );
}

然後要實現 get_todos,在 src/repository/database.rs

impl Database {
    // 省略

    pub fn get_todos(&self) -> Vec<Todo> {
        let todos = self.todos.lock().unwrap();
        todos.clone()
    }
}

GET /todos/{id

也是先在 src/api/api.rs 新增:

#[get("/todos/{id}")]
pub async fn get_todo_by_id(db: web::Data<Database>, id: web::Path<String>) -> HttpResponse {
    let todo = db.get_todo_by_id(&id);
    match todo {
        Some(todo) => HttpResponse::Ok().json(todo),
        None => HttpResponse::NotFound().body("Todo not found"),
    }
}

pub fn config(cfg: &mut web::ServiceConfig) {
    cfg.service(
        web::scope("/api")
            .service(create_todo)
            .service(get_todos)
            .service(get_todo_by_id)
    );
}

然後在 src/repositorydatabase.rs 實現 get_todo_by_id

impl Database {
    // 省略

    pub fn get_todo_by_id(&self, id: &str) -> Option<Todo> {
        let todos = self.todos.lock().unwrap();
        todos
            .iter()
            .find(|todo| todo.id == Some(id.to_string()))
            .cloned()
    }
}

PUT /todos/{id}

src/api/api.rs 新增:

use actix_web::{
    get, post, put,
    web::{Data, Json},
    HttpResponse,
};

// 省略

#[put("/todos/{id}")]
pub async fn update_todo_by_id(
    db: web::Data<Database>,
    id: web::Path<String>,
    updated_todo: web::Json<Todo>,
) -> HttpResponse {
    let todo = db.update_todo_by_id(&id, updated_todo.into_inner());
    match todo {
        Some(todo) => HttpResponse::Ok().json(todo),
        None => HttpResponse::NotFound().body("Todo not found"),
    }
}

pub fn config(cfg: &mut web::ServiceConfig) {
    cfg.service(
        web::scope("/api")
            .service(create_todo)
            .service(get_todos)
            .service(get_todo_by_id)
            .service(update_todo_by_id),
    );
}

src/repository/database.rs 實現 update_todo_by_id

impl Database {
    // 省略

    pub fn update_todo_by_id(&self, id: &str, todo: Todo) -> Option<Todo> {
        let mut todos = self.todos.lock().unwrap();
        let updated_at = Utc::now();
        let todo = Todo {
            id: Some(id.to_string()),
            updated_at: Some(updated_at),
            ..todo
        };
        let index = todos
            .iter()
            .position(|todo| todo.id == Some(id.to_string()))?;
        todos[index] = todo.clone();
        Some(todo)
    }
}

DELETE /todos/{id}

use actix_web::{
    delete, get, post, put,
    web::{Data, Json},
    HttpResponse,
};

// 省略

#[delete("/todos/{id}")]
pub async fn delete_todo_by_id(db: web::Data<Database>, id: web::Path<String>) -> HttpResponse {
    let todo = db.delete_todo_by_id(&id);
    match todo {
        Some(todo) => HttpResponse::Ok().json(todo),
        None => HttpResponse::NotFound().body("Todo not found"),
    }
}

pub fn config(cfg: &mut web::ServiceConfig) {
    cfg.service(
        web::scope("/api")
            .service(create_todo)
            .service(get_todos)
            .service(get_todo_by_id)
            .service(update_todo_by_id)
            .service(delete_todo_by_id),
    );
}

src/repository/database.rs 實現 delete_todo_by_id

impl Database {
    // 省略

    pub fn delete_todo_by_id(&self, id: &str) -> Option<Todo> {
        let mut todos = self.todos.lock().unwrap();
        let index = todos
            .iter()
            .position(|todo| todo.id == Some(id.to_string()))?;
        Some(todos.remove(index))
    }
}

在這篇文章中,我們示範了如何使用 Actix Web 建立 REST API 。Actix Web 不僅為 Rust 開發者提供了強大的工具,還確保了我們的 API 具有卓越的性能和靈活性。隨著 Rust 在網頁開發領域的逐步崛起,相信 Actix Web 會成為更多開發者的首選。

推薦學習
為你自己學 JavaScript-cover

為你自己學 JavaScript

你知道如何靈活運用 JavaScript 嗎? JavaScript 主要運行於瀏覽器中的動態腳本語言,透過它可以處理與瀏覽器的互動。 JavaScript 簡單上手、應用範圍廣泛,容易有成就感,但精通不易。 隨著前端技術的...

查看課程