สร้าง Web API ด้วย Wisp 2.1.1 ในภาษา Gleam

บทนำ

Gleam เป็นภาษาโปรแกรมแบบ Functional ที่ทำงานบน BEAM (Erlang VM) มีระบบ Type ที่เข้มงวด ช่วยให้โค้ดปลอดภัยและบำรุงรักษาง่าย Wisp คือ Web Framework สำหรับ Gleam ที่ออกแบบมาให้พัฒนาได้รวดเร็วและดูแลรักษาง่าย โดยมีแนวคิดหลักคือ Handlers และ Middleware

บทความนี้จะพาคุณสร้าง RESTful API ตั้งแต่เริ่มต้นจนใช้งานได้จริง โดยใช้ Wisp เวอร์ชัน 2.1.1

สิ่งที่ต้องเตรียม

ขั้นตอนที่ 1: สร้างโปรเจกต์ใหม่

เริ่มต้นด้วยการสร้างโปรเจกต์ Gleam ใหม่:

gleam new my_api
cd my_api

ขั้นตอนที่ 2: ติดตั้ง Dependencies

เพิ่ม packages ที่จำเป็นสำหรับการสร้าง Web API:

gleam add wisp@2.1.1
gleam add mist
gleam add gleam_http
gleam add gleam_json
gleam add gleam_erlang

รายละเอียดของแต่ละ package:

Package หน้าที่
wisp Web Framework หลัก
mist HTTP Server สำหรับ Gleam
gleam_http Types และ functions สำหรับ HTTP
gleam_json Encode/Decode JSON
gleam_erlang ฟังก์ชันสำหรับ Erlang runtime

ขั้นตอนที่ 3: โครงสร้างโปรเจกต์

my_api/
├── src/
│   ├── my_api.gleam       # Entry point
│   ├── my_api/
│   │   ├── router.gleam   # จัดการ routing
│   │   ├── web.gleam      # Middleware
│   │   └── handlers/
│   │       └── user.gleam # User handlers
├── test/
└── gleam.toml

ขั้นตอนที่ 4: สร้าง Entry Point

แก้ไขไฟล์ src/my_api.gleam:

import gleam/erlang/process
import mist
import wisp
import wisp/wisp_mist
import my_api/router

pub fn main() {
  // ตั้งค่า logger
  wisp.configure_logger()
  
  // Secret key สำหรับ cookies และข้อมูลที่ต้องเซ็น
  let secret_key_base = wisp.random_string(64)
  
  // สร้างและเริ่มต้น server
  let assert Ok(_) =
    wisp_mist.handler(router.handle_request, secret_key_base)
    |> mist.new
    |> mist.port(8080)
    |> mist.start_http
  
  // รักษา process ให้ทำงานต่อเนื่อง
  process.sleep_forever()
}

ขั้นตอนที่ 5: สร้าง Middleware

สร้างไฟล์ src/my_api/web.gleam:

import wisp.{type Request, type Response}

/// Middleware หลักที่ใช้กับทุก request
pub fn middleware(
  req: Request,
  handle_request: fn(Request) -> Response,
) -> Response {
  // เปิดใช้ method override (สำหรับ forms ที่ต้องการใช้ PUT/DELETE)
  let req = wisp.method_override(req)
  
  // Log ทุก request
  use <- wisp.log_request(req)
  
  // จัดการ crashes อัตโนมัติ
  use <- wisp.rescue_crashes
  
  // จัดการ HEAD requests
  use req <- wisp.handle_head(req)
  
  // ป้องกัน CSRF
  use req <- wisp.csrf_known_header_protection(req)
  
  handle_request(req)
}

ขั้นตอนที่ 6: สร้าง Router

สร้างไฟล์ src/my_api/router.gleam:

import gleam/http.{Delete, Get, Post, Put}
import wisp.{type Request, type Response}
import my_api/web
import my_api/handlers/user

/// Handler หลักที่รับทุก request
pub fn handle_request(req: Request) -> Response {
  use req <- web.middleware(req)
  
  // ใช้ pattern matching สำหรับ routing
  // Wisp ไม่มี router abstraction พิเศษ แต่ใช้ pattern matching แทน
  case wisp.path_segments(req) {
    // GET /
    [] -> home(req)
    
    // /api/users
    ["api", "users"] -> user.handle_users(req)
    
    // /api/users/:id
    ["api", "users", id] -> user.handle_user(req, id)
    
    // /health
    ["health"] -> health_check(req)
    
    // 404 สำหรับ routes อื่นๆ
    _ -> wisp.not_found()
  }
}

fn home(req: Request) -> Response {
  use <- wisp.require_method(req, Get)
  
  wisp.ok()
  |> wisp.string_body("Welcome to My API - Built with Wisp 2.1.1")
}

fn health_check(req: Request) -> Response {
  use <- wisp.require_method(req, Get)
  
  wisp.json_response(
    "{\"status\": \"healthy\", \"version\": \"1.0.0\"}",
    200,
  )
}

ขั้นตอนที่ 7: สร้าง User Handlers

สร้างไฟล์ src/my_api/handlers/user.gleam:

import gleam/dynamic
import gleam/http.{Delete, Get, Post, Put}
import gleam/int
import gleam/json
import gleam/list
import gleam/option.{None, Some}
import gleam/result
import wisp.{type Request, type Response}

/// User type
pub type User {
  User(id: Int, name: String, email: String)
}

/// จำลองข้อมูล users (ในการใช้งานจริงควรใช้ database)
fn get_mock_users() -> List(User) {
  [
    User(1, "สมชาย ใจดี", "somchai@example.com"),
    User(2, "สมหญิง รักเรียน", "somying@example.com"),
    User(3, "John Doe", "john@example.com"),
  ]
}

/// Handle requests ไปที่ /api/users
pub fn handle_users(req: Request) -> Response {
  case req.method {
    Get -> list_users(req)
    Post -> create_user(req)
    _ -> wisp.method_not_allowed([Get, Post])
  }
}

/// Handle requests ไปที่ /api/users/:id
pub fn handle_user(req: Request, id: String) -> Response {
  case req.method {
    Get -> get_user(req, id)
    Put -> update_user(req, id)
    Delete -> delete_user(req, id)
    _ -> wisp.method_not_allowed([Get, Put, Delete])
  }
}

/// GET /api/users - รายการ users ทั้งหมด
fn list_users(_req: Request) -> Response {
  let users = get_mock_users()
  let json_body = users_to_json(users)
  
  wisp.json_response(json_body, 200)
}

/// GET /api/users/:id - ดึงข้อมูล user ตาม id
fn get_user(_req: Request, id: String) -> Response {
  case int.parse(id) {
    Ok(user_id) -> {
      let users = get_mock_users()
      case list.find(users, fn(u) { u.id == user_id }) {
        Ok(user) -> wisp.json_response(user_to_json(user), 200)
        Error(_) -> {
          wisp.json_response(
            error_json("User not found"),
            404,
          )
        }
      }
    }
    Error(_) -> {
      wisp.json_response(
        error_json("Invalid user ID"),
        400,
      )
    }
  }
}

/// POST /api/users - สร้าง user ใหม่
fn create_user(req: Request) -> Response {
  use json_body <- wisp.require_json(req)
  
  // Decode JSON body
  let decoder =
    dynamic.decode2(
      fn(name, email) { User(4, name, email) },
      dynamic.field("name", dynamic.string),
      dynamic.field("email", dynamic.string),
    )
  
  case decoder(json_body) {
    Ok(user) -> {
      wisp.json_response(user_to_json(user), 201)
    }
    Error(_) -> {
      wisp.json_response(
        error_json("Invalid request body"),
        400,
      )
    }
  }
}

/// PUT /api/users/:id - อัพเดท user
fn update_user(req: Request, id: String) -> Response {
  use json_body <- wisp.require_json(req)
  
  case int.parse(id) {
    Ok(user_id) -> {
      let decoder =
        dynamic.decode2(
          fn(name, email) { User(user_id, name, email) },
          dynamic.field("name", dynamic.string),
          dynamic.field("email", dynamic.string),
        )
      
      case decoder(json_body) {
        Ok(user) -> {
          wisp.json_response(user_to_json(user), 200)
        }
        Error(_) -> {
          wisp.json_response(
            error_json("Invalid request body"),
            400,
          )
        }
      }
    }
    Error(_) -> {
      wisp.json_response(
        error_json("Invalid user ID"),
        400,
      )
    }
  }
}

/// DELETE /api/users/:id - ลบ user
fn delete_user(_req: Request, id: String) -> Response {
  case int.parse(id) {
    Ok(_user_id) -> {
      wisp.json_response(
        "{\"message\": \"User deleted successfully\"}",
        200,
      )
    }
    Error(_) -> {
      wisp.json_response(
        error_json("Invalid user ID"),
        400,
      )
    }
  }
}

/// แปลง User เป็น JSON string
fn user_to_json(user: User) -> String {
  json.object([
    #("id", json.int(user.id)),
    #("name", json.string(user.name)),
    #("email", json.string(user.email)),
  ])
  |> json.to_string
}

/// แปลง List ของ Users เป็น JSON string
fn users_to_json(users: List(User)) -> String {
  json.array(users, fn(user) {
    json.object([
      #("id", json.int(user.id)),
      #("name", json.string(user.name)),
      #("email", json.string(user.email)),
    ])
  })
  |> json.to_string
}

/// สร้าง error JSON response
fn error_json(message: String) -> String {
  json.object([
    #("error", json.string(message)),
  ])
  |> json.to_string
}

ขั้นตอนที่ 8: รันและทดสอบ

รัน Server

gleam run

คุณจะเห็นข้อความ:

Listening on http://127.0.0.1:8080

ทดสอบ API ด้วย curl

# ทดสอบ Home
curl http://localhost:8080

# ทดสอบ Health Check
curl http://localhost:8080/health

# ดึงรายการ Users ทั้งหมด
curl http://localhost:8080/api/users

# ดึง User ตาม ID
curl http://localhost:8080/api/users/1

# สร้าง User ใหม่
curl -X POST http://localhost:8080/api/users \
  -H "Content-Type: application/json" \
  -d '{"name": "New User", "email": "new@example.com"}'

# อัพเดท User
curl -X PUT http://localhost:8080/api/users/1 \
  -H "Content-Type: application/json" \
  -d '{"name": "Updated Name", "email": "updated@example.com"}'

# ลบ User
curl -X DELETE http://localhost:8080/api/users/1

คุณสมบัติเด่นของ Wisp 2.1.1

1. Type-Safe Routing

ใช้ Pattern Matching ของ Gleam ในการจัดการ routes ทำให้ปลอดภัยและรวดเร็วกว่า router แบบดั้งเดิม

2. Composable Middleware

Middleware สามารถ compose กันได้อย่างยืดหยุ่นโดยใช้ use syntax ของ Gleam

3. Built-in Security Features

4. JSON Handling

รองรับการ parse และสร้าง JSON ได้ง่ายผ่าน gleam_json package

5. รองรับ Static Files

use <- wisp.serve_static(req, under: "/static", from: "/public")

เคล็ดลับและ Best Practices

1. จัดการ Environment Variables

import envoy

pub fn main() {
  let secret = 
    envoy.get("SECRET_KEY_BASE")
    |> result.unwrap("default_secret_for_dev")
  // ...
}

2. Context Pattern

สร้าง Context type เพื่อส่งข้อมูลที่ใช้ร่วมกัน เช่น database connection:

pub type Context {
  Context(
    db: Database,
    secret: String,
  )
}

pub fn handle_request(req: Request, ctx: Context) -> Response {
  // ใช้ ctx.db ในการ query database
  wisp.ok()
}

3. Error Handling แบบ Functional

import gleam/result.{try}

pub fn create_item(req: Request, ctx: Context) -> Response {
  use json <- wisp.require_json(req)
  
  let result = {
    use params <- try(parse_params(json))
    use item <- try(save_to_db(params, ctx.db))
    Ok(item_to_json(item))
  }
  
  case result {
    Ok(body) -> wisp.json_response(body, 201)
    Error(_) -> wisp.bad_request()
  }
}

สรุป

Wisp 2.1.1 เป็น Web Framework ที่เรียบง่ายแต่ทรงพลัง เหมาะสำหรับการสร้าง Web API ด้วยภาษา Gleam โดยมีข้อดีหลักคือ:

แหล่งข้อมูลเพิ่มเติม


บทความนี้ใช้ Wisp เวอร์ชัน 2.1.1 (Released: December 11, 2025)