การสร้าง Web Application สมัยใหม่ด้วย Gleam & Dream

Building Modern Web Apps with Gleam & Dream

คู่มือฉบับสมบูรณ์ สำหรับนักพัฒนาที่ต้องการสร้าง Web Application แบบ Type-safe, Fault-tolerant บน BEAM Virtual Machine ด้วยภาษา Gleam และ Web Toolkit ชื่อ Dream


1. บทนำสู่ Dream และระบบนิเวศ Gleam Web (Introduction to Dream & the Gleam Web Ecosystem)

1.1 ปรัชญาของ Dream — "ไม่มีเวทมนตร์ ทุกอย่างชัดเจน" (Dream's Philosophy — "No Magic, Everything Explicit")

Dream คือ Web Toolkit สำหรับภาษา Gleam ที่ออกแบบมาบนหลักการสำคัญ: ความชัดเจนเหนือความสะดวก (Explicit over Implicit)

ในขณะที่ Framework ทั่วไปอย่าง Rails, Django หรือ Laravel ใช้แนวคิด "Convention over Configuration" ซึ่งซ่อนรายละเอียดไว้เบื้องหลัง Dream เลือกเปิดเผยทุกอย่างออกมาอย่างโปร่งใส

1.1.1 หลักการสามประการของ Dream

1.1.2 เปรียบเทียบกับ Framework ทั่วไป

คุณสมบัติ Dream (Gleam) Express (Node.js) Phoenix (Elixir) FastAPI (Python)
Type Safety ✅ Compile-time ❌ Runtime only ⚠️ Partial ⚠️ Partial
Magic/Annotation ❌ ไม่มี ⚠️ บางส่วน ✅ มาก ✅ มาก
Middleware Explicit function Implicit pipeline Plug pipeline Decorator
DI Pattern Explicit Services arg ❌ ไม่มีมาตรฐาน ⚠️ Assigns ❌ Global state
Concurrency BEAM Actor Event loop BEAM Actor Async/Await
Fault Tolerance OTP Supervision ❌ Process crash OTP Supervision ❌ ไม่มีมาตรฐาน

1.2 ข้อได้เปรียบของ BEAM (The BEAM Advantage)

BEAM (Bogdan/Björn's Erlang Abstract Machine) คือ Virtual Machine ที่ถูกสร้างมาเพื่อรองรับระบบ Distributed, Fault-tolerant ตั้งแต่ต้น ไม่ใช่สิ่งที่เพิ่มเข้ามาทีหลัง

1.2.1 สถาปัตยกรรม Erlang/OTP

%%{init: {'theme': 'base', 'themeVariables': {
  'primaryColor': '#458588',
  'primaryTextColor': '#ebdbb2',
  'primaryBorderColor': '#83a598',
  'lineColor': '#fabd2f',
  'secondaryColor': '#3c3836',
  'tertiaryColor': '#504945',
  'background': '#282828',
  'mainBkg': '#3c3836',
  'nodeBorder': '#83a598',
  'clusterBkg': '#32302f',
  'titleColor': '#ebdbb2',
  'edgeLabelBackground': '#504945',
  'fontFamily': 'monospace'
}}}%%
graph TB
    subgraph BEAM["BEAM Virtual Machine"]
        subgraph OTP["OTP Framework"]
            SUP["Supervisor Tree
ต้นไม้ควบคุม"] GEN["GenServer
Actor ทั่วไป"] ACT["Actor Model
โมเดลการทำงาน"] end subgraph SCHED["Scheduler"] S1["Scheduler 1
CPU Core 1"] S2["Scheduler 2
CPU Core 2"] SN["Scheduler N
CPU Core N"] end subgraph PROC["Lightweight Processes
กระบวนการน้ำหนักเบา"] P1["Process 1"] P2["Process 2"] PN["Process N (millions)
ล้านกระบวนการ"] end end SUP --> GEN GEN --> ACT ACT --> PROC SCHED --> PROC S1 & S2 & SN --> P1 & P2 & PN

1.2.2 Fault Tolerance ในชีวิตจริง

"Let it crash" คือปรัชญาของ Erlang/OTP — เมื่อ Process ล้มเหลว Supervisor จะ Restart ให้อัตโนมัติ ระบบโดยรวมยังคงทำงานต่อได้

ตัวอย่างบริษัทที่ใช้ BEAM ในระบบ Production จริง:

1.2.3 ข้อได้เปรียบด้าน Concurrency เทียบกับแนวทางอื่น

%%{init: {'theme': 'base', 'themeVariables': {
  'primaryColor': '#98971a',
  'primaryTextColor': '#ebdbb2',
  'primaryBorderColor': '#b8bb26',
  'lineColor': '#fabd2f',
  'secondaryColor': '#3c3836',
  'background': '#282828',
  'mainBkg': '#3c3836',
  'clusterBkg': '#32302f',
  'titleColor': '#fabd2f',
  'fontFamily': 'monospace'
}}}%%
graph LR
    subgraph NODE["Node.js — Single Thread"]
        EV["Event Loop
วนซ้ำเดียว"] CB["Callbacks/Promises"] EV --> CB end subgraph THREAD["Java/Go — OS Threads"] T1["Thread 1
~1MB RAM"] T2["Thread 2
~1MB RAM"] TN["Thread N
~1MB RAM"] end subgraph BEAM2["BEAM — Green Processes"] BP1["Process 1
~300 bytes"] BP2["Process 2
~300 bytes"] BPN["Process N
~300 bytes
ล้านตัวพร้อมกัน"] end style NODE fill:#3c3836,stroke:#fb4934 style THREAD fill:#3c3836,stroke:#fabd2f style BEAM2 fill:#3c3836,stroke:#b8bb26

1.3 ภาพรวมของ Stack — Dream, Mist และ Ecosystem (Stack Overview)

1.3.1 สถาปัตยกรรมของ Dream Stack

%%{init: {'theme': 'base', 'themeVariables': {
  'primaryColor': '#b16286',
  'primaryTextColor': '#ebdbb2',
  'primaryBorderColor': '#d3869b',
  'lineColor': '#8ec07c',
  'secondaryColor': '#3c3836',
  'background': '#282828',
  'mainBkg': '#3c3836',
  'clusterBkg': '#32302f',
  'titleColor': '#ebdbb2',
  'fontFamily': 'monospace'
}}}%%
graph TB
    CLIENT["🌐 HTTP Client
Browser / cURL / Fetch"] subgraph APP["แอปพลิเคชัน Gleam"] ROUTER["dream/router
จัดเส้นทาง HTTP"] MW["Middleware Stack
Logger, Auth, CORS"] CTRL["Controllers
ตัวจัดการ Request"] SVC["Services
DB, Cache, Email"] end subgraph SERVER["HTTP Server Layer"] MIST["Mist
HTTP/1.1 + HTTP/2 Server"] end subgraph RUNTIME["BEAM Runtime"] OTP2["OTP Supervision Tree"] SCHED2["Schedulers (per CPU)"] end CLIENT <--> MIST MIST <--> ROUTER ROUTER --> MW --> CTRL CTRL <--> SVC MIST & APP --> OTP2 OTP2 --> SCHED2

1.3.2 ไลบรารีหลักใน Ecosystem

ไลบรารี บทบาท hex.pm
dream Web toolkit หลัก gleam_dream
mist HTTP server (HTTP/1.1, HTTP/2, WebSocket) mist
gleam_http HTTP types (Request, Response, Method) gleam_http
gleam_json JSON encode/decode แบบ Type-safe gleam_json
gleam/otp Actor, Process, Supervisor gleam_otp
gleam_pgo PostgreSQL driver gleam_pgo
nakai HTML DSL สำหรับ Server-side rendering nakai
lustre Frontend framework (Elm architecture) lustre
gleeunit Testing framework gleeunit

1.4 การติดตั้ง Development Environment

1.4.1 ติดตั้ง Gleam และ Erlang/OTP

# ติดตั้ง asdf version manager (แนะนำ)
# Install asdf version manager (recommended)
git clone https://github.com/asdf-vm/asdf.git ~/.asdf --branch v0.14.0
echo '. "$HOME/.asdf/asdf.sh"' >> ~/.bashrc
source ~/.bashrc

# ติดตั้ง Erlang/OTP
# Install Erlang/OTP
asdf plugin add erlang
asdf install erlang 26.2.5
asdf global erlang 26.2.5

# ติดตั้ง Gleam
# Install Gleam
asdf plugin add gleam
asdf install gleam 1.4.1
asdf global gleam 1.4.1

# ตรวจสอบการติดตั้ง
# Verify installation
gleam --version    # gleam 1.4.1
erl +V             # Erlang/OTP 26

1.4.2 สร้างโปรเจกต์ใหม่

# สร้างโปรเจกต์ Dream ใหม่
# Create a new Dream project
gleam new my_web_app
cd my_web_app

# โครงสร้างโปรเจกต์เริ่มต้น
# Initial project structure
tree .
# my_web_app/
# ├── gleam.toml          ← ไฟล์ config หลัก
# ├── src/
# │   └── my_web_app.gleam
# ├── test/
# │   └── my_web_app_test.gleam
# └── README.md

1.4.3 โครงสร้างโปรเจกต์ที่แนะนำ

my_web_app/
├── gleam.toml
├── src/
│   ├── my_web_app.gleam          ← Entry point
│   ├── router.gleam              ← Route definitions
│   ├── middleware.gleam          ← Custom middleware
│   ├── services.gleam            ← Services type + factory
│   ├── controllers/
│   │   ├── user_controller.gleam
│   │   └── post_controller.gleam
│   ├── repositories/
│   │   ├── user_repository.gleam
│   │   └── post_repository.gleam
│   └── models/
│       ├── user.gleam
│       └── post.gleam
└── test/
    ├── controllers/
    └── repositories/

2. เซิร์ฟเวอร์ Dream แรกของคุณ (Your First Dream Server)

2.1 โครงสร้างโปรเจกต์และ Dependencies

2.1.1 กำหนดค่า gleam.toml

# gleam.toml — ไฟล์กำหนดค่าหลักของโปรเจกต์
# gleam.toml — Main project configuration file
name = "my_web_app"
version = "1.0.0"
description = "Web application built with Dream"

[dependencies]
gleam_stdlib = ">= 0.34.0 and < 2.0.0"
# Dream — Web toolkit หลัก
dream = ">= 0.1.0 and < 1.0.0"
# Mist — HTTP Server ที่ Dream ใช้
mist = ">= 3.0.0 and < 4.0.0"
# Gleam HTTP types
gleam_http = ">= 3.6.0 and < 4.0.0"
# JSON handling
gleam_json = ">= 2.0.0 and < 3.0.0"
# OTP for concurrency
gleam_otp = ">= 0.10.0 and < 1.0.0"
# Erlang FFI
gleam_erlang = ">= 0.25.0 and < 1.0.0"

[dev-dependencies]
gleeunit = ">= 1.2.0 and < 2.0.0"

2.1.2 ติดตั้ง Dependencies

# ดึง dependencies ทั้งหมด
# Fetch all dependencies
gleam deps download

# รันโปรเจกต์
# Run the project
gleam run

# รัน tests
# Run tests
gleam test

2.2 Controller Model — หัวใจของ Dream

2.2.1 ทำความเข้าใจ fn controller(request, context, services)

Dream แยก argument ออกเป็นสามส่วนเพื่อความชัดเจน:

// src/controllers/user_controller.gleam

import dream/http.{type Request, type Response}
import my_web_app/services.{type Services}
import my_web_app/context.{type Context}
import gleam/json
import gleam/result

// ฟังก์ชัน Controller มาตรฐานของ Dream
// Standard Dream controller function signature
pub fn get_user(
  request: Request,         // HTTP Request (immutable)
  context: Context,         // Per-request context (auth info, etc.)
  services: Services,       // Shared services (db, cache, etc.)
) -> Response {
  // ดึง user_id จาก path parameter
  // Extract user_id from path parameter
  let user_id = context.path_params
    |> dict.get("id")
    |> result.unwrap("unknown")

  // ใช้ services.db เพื่อดึงข้อมูล
  // Use services.db to fetch data
  case services.user_repo.find_by_id(user_id) {
    Ok(user) ->
      user
      |> user.to_json()
      |> json.to_string()
      |> dream_http.json_response(status: dream_http.ok)
    
    Error(NotFound) ->
      dream_http.json_response(
        body: "{\"error\": \"User not found\"}",
        status: dream_http.not_found,
      )
    
    Error(_) ->
      dream_http.json_response(
        body: "{\"error\": \"Internal server error\"}",
        status: dream_http.internal_server_error,
      )
  }
}

2.3 การกำหนด Routes

2.3.1 สร้าง Router หลัก

// src/router.gleam

import dream/router.{type Router}
import dream/http
import my_web_app/controllers/user_controller
import my_web_app/controllers/post_controller
import my_web_app/middleware

// กำหนด Router หลักของแอปพลิเคชัน
// Define the main application router
pub fn router() -> Router {
  router.new()
  // กลุ่ม Route สำหรับ User API
  // User API routes group
  |> router.route(
    method: http.Get,
    path: "/api/users",
    controller: user_controller.list_users,
    middleware: [middleware.logger, middleware.auth_required],
  )
  |> router.route(
    method: http.Get,
    path: "/api/users/:id",          // :id คือ path parameter
    controller: user_controller.get_user,
    middleware: [middleware.logger],
  )
  |> router.route(
    method: http.Post,
    path: "/api/users",
    controller: user_controller.create_user,
    middleware: [middleware.logger, middleware.auth_required, middleware.json_body],
  )
  // Health check endpoint
  |> router.route(
    method: http.Get,
    path: "/health",
    controller: fn(_, _, _) { dream_http.text_response("OK", dream_http.ok) },
    middleware: [],
  )
}

2.4 การเริ่มเซิร์ฟเวอร์

2.4.1 Entry Point หลัก

// src/my_web_app.gleam

import dream/servers/mist/server
import my_web_app/router
import my_web_app/services

pub fn main() {
  // สร้าง Services (database, cache, ฯลฯ)
  // Create application services (database, cache, etc.)
  let services = services.create()

  // เริ่มเซิร์ฟเวอร์ด้วย pipe operator
  // Start server using pipe operator
  server.new()
  |> server.router(router.router())          // กำหนด Router
  |> server.services(services)               // ฉีด Services เข้าไป
  |> server.port(8080)                       // กำหนด Port
  |> server.listen()                         // เริ่มฟัง connection
}

2.5 วงจร Request-Response (The Request-Response Cycle)

2.5.1 ประเภทหลักของ HTTP

// ประเภทสำคัญใน dream/http
// Important types in dream/http

// Request — ข้อมูล HTTP Request ที่เข้ามา
// Request — Incoming HTTP Request data
pub type Request {
  Request(
    method: Method,                    // GET, POST, PUT, DELETE, ...
    path: String,                      // "/api/users/123"
    query: Option(String),             // "?page=1&limit=20"
    headers: List(#(String, String)),  // [("Content-Type", "application/json")]
    body: RequestBody,                 // String, Binary, Stream
  )
}

// Response — ข้อมูล HTTP Response ที่ส่งกลับ
// Response — Outgoing HTTP Response data  
pub type Response {
  Response(
    status: Int,                       // 200, 404, 500
    headers: List(#(String, String)),  // [("Content-Type", "application/json")]
    body: ResponseBody,                // String, Binary, Stream
  )
}

2.5.2 Status Constants ที่ใช้บ่อย

// Status constants ใน dream/http
// HTTP Status constants in dream/http

// 2xx — สำเร็จ (Success)
dream_http.ok                  // 200
dream_http.created             // 201
dream_http.accepted            // 202
dream_http.no_content          // 204

// 3xx — เปลี่ยนเส้นทาง (Redirection)
dream_http.moved_permanently   // 301
dream_http.found               // 302

// 4xx — Client Error
dream_http.bad_request         // 400
dream_http.unauthorized        // 401
dream_http.forbidden           // 403
dream_http.not_found           // 404
dream_http.teapot              // 418 — "I'm a teapot" (RFC 2324)

// 5xx — Server Error
dream_http.internal_server_error  // 500
dream_http.service_unavailable    // 503

3. Routing เชิงลึก (Routing in Depth)

3.1 Routes แบบ Static และ Dynamic

3.1.1 Path Parameters และ Wildcard Routes

// src/router.gleam

pub fn router() -> Router {
  router.new()
  
  // --- Static Routes (เส้นทางคงที่) ---
  |> router.route(method: http.Get, path: "/",          controller: home_ctrl.index,   middleware: [])
  |> router.route(method: http.Get, path: "/about",     controller: home_ctrl.about,   middleware: [])
  |> router.route(method: http.Get, path: "/contact",   controller: home_ctrl.contact, middleware: [])
  
  // --- Dynamic Routes (เส้นทางแบบไดนามิก) ---
  // :id จะถูกจับเป็น path parameter ชื่อ "id"
  // :id is captured as a path parameter named "id"
  |> router.route(method: http.Get,    path: "/users/:id",               controller: user_ctrl.show,   middleware: [])
  |> router.route(method: http.Put,    path: "/users/:id",               controller: user_ctrl.update, middleware: [])
  |> router.route(method: http.Delete, path: "/users/:id",               controller: user_ctrl.delete, middleware: [])
  
  // Nested path parameters — หลาย parameter
  // Multiple path parameters
  |> router.route(method: http.Get, path: "/users/:user_id/posts/:post_id",
    controller: post_ctrl.show_user_post, middleware: [])
  
  // Wildcard route — จับทุกเส้นทาง
  // Wildcard — catch all paths
  |> router.route(method: http.Get, path: "/*",
    controller: home_ctrl.not_found, middleware: [])
}

// การอ่าน path parameters ใน Controller
// Reading path parameters in Controller
pub fn show(request: Request, context: Context, services: Services) -> Response {
  let user_id = case context.path_params |> dict.get("id") {
    Ok(id) -> id
    Error(_) -> panic as "Missing required path parameter :id"
  }
  // ใช้ user_id ต่อไป...
  // Continue using user_id...
}

3.2 Query Parameters และ URL Helpers

3.2.1 Parse และ Validate URL Parameters แบบ Type-safe

// src/controllers/post_controller.gleam

import gleam/uri
import gleam/int
import gleam/option.{type Option, None, Some}

// Type สำหรับ Pagination parameters
// Type for Pagination parameters
pub type PaginationParams {
  PaginationParams(page: Int, limit: Int, sort: Option(String))
}

// Parse query parameters อย่างปลอดภัย
// Safely parse query parameters
fn parse_pagination(request: Request) -> Result(PaginationParams, String) {
  let query_string = request.query |> option.unwrap("")
  let params = uri.parse_query(query_string) |> result.unwrap([])
  
  // ดึงค่า page (default: 1)
  // Get page value (default: 1)
  let page = params
    |> list.key_find("page")
    |> result.try(int.parse)
    |> result.unwrap(1)
  
  // ดึงค่า limit (default: 20, max: 100)
  // Get limit value (default: 20, max: 100)
  let limit = params
    |> list.key_find("limit")
    |> result.try(int.parse)
    |> result.unwrap(20)
    |> int.min(100)   // จำกัดไม่เกิน 100 รายการ / Cap at 100 items
  
  // ดึงค่า sort (optional)
  // Get sort value (optional)
  let sort = params |> list.key_find("sort") |> option.from_result()
  
  Ok(PaginationParams(page:, limit:, sort:))
}

// ใช้งานใน Controller
// Usage in Controller
pub fn list_posts(request: Request, context: Context, services: Services) -> Response {
  case parse_pagination(request) {
    Ok(params) ->
      services.post_repo.list(page: params.page, limit: params.limit)
      |> result.map(posts_to_json_response)
      |> result.unwrap_both()
    
    Error(msg) ->
      dream_http.json_response(
        body: json.object([#("error", json.string(msg))]) |> json.to_string(),
        status: dream_http.bad_request,
      )
  }
}

3.3 การจัดระเบียบ Routes

3.3.1 แยก Routes เป็น Module ย่อย

// src/router.gleam — Router หลักรวมทุก Module

import my_web_app/routes/user_routes
import my_web_app/routes/post_routes
import my_web_app/routes/auth_routes

pub fn router() -> Router {
  router.new()
  |> router.merge(auth_routes.routes())   // /auth/*
  |> router.merge(user_routes.routes())   // /api/users/*
  |> router.merge(post_routes.routes())   // /api/posts/*
}

// src/routes/user_routes.gleam — User Routes Module
pub fn routes() -> Router {
  router.new()
  |> router.route(method: http.Get,    path: "/api/users",     controller: user_ctrl.index,  middleware: [mw.auth])
  |> router.route(method: http.Get,    path: "/api/users/:id", controller: user_ctrl.show,   middleware: [mw.auth])
  |> router.route(method: http.Post,   path: "/api/users",     controller: user_ctrl.create, middleware: [mw.auth, mw.admin])
  |> router.route(method: http.Put,    path: "/api/users/:id", controller: user_ctrl.update, middleware: [mw.auth])
  |> router.route(method: http.Delete, path: "/api/users/:id", controller: user_ctrl.delete, middleware: [mw.auth, mw.admin])
}

3.4 HTTP Methods และ RESTful Conventions

3.4.1 ตาราง RESTful Route Pattern

HTTP Method Path Controller Action คำอธิบาย
GET /resources index ดึงรายการทั้งหมด
POST /resources create สร้างรายการใหม่
GET /resources/:id show ดึงรายการตาม ID
PUT /resources/:id update อัปเดตรายการทั้งหมด
PATCH /resources/:id patch อัปเดตรายการบางส่วน
DELETE /resources/:id delete ลบรายการ
GET /resources/new new ฟอร์มสร้างใหม่ (HTML)
GET /resources/:id/edit edit ฟอร์มแก้ไข (HTML)

4. Middleware — ชัดเจน ไม่ใช่เวทมนตร์ (Middleware — Explicit, Not Magic)

4.1 ปรัชญา Middleware ของ Dream

Middleware ใน Dream คือฟังก์ชันธรรมดาที่รับ Handler เข้ามาและคืน Handler ออกไป ไม่มี global registration, ไม่มี hidden pipeline

// นิยาม Type ของ Middleware
// Middleware type definition
pub type Middleware =
  fn(Handler) -> Handler

// Handler คือฟังก์ชันที่รับ Request และคืน Response
// Handler is a function taking Request and returning Response
pub type Handler =
  fn(Request, Context, Services) -> Response

4.2 Built-in Middleware

4.2.1 Logger Middleware

// dream/middleware.gleam (built-in)

// Logger — บันทึก HTTP request ทุกรายการ
// Logger — Log every HTTP request
pub fn logger(handler: Handler) -> Handler {
  fn(request: Request, context: Context, services: Services) -> Response {
    let start_time = erlang.system_time(erlang.Millisecond)
    
    let response = handler(request, context, services)
    
    let duration = erlang.system_time(erlang.Millisecond) - start_time
    
    // บันทึก: [GET] /api/users 200 45ms
    // Log: [GET] /api/users 200 45ms
    logger.info(
      "[" <> http.method_to_string(request.method) <> "] " <>
      request.path <> " " <>
      int.to_string(response.status) <> " " <>
      int.to_string(duration) <> "ms"
    )
    
    response
  }
}

4.2.2 Recovery Middleware

// Recovery — จัดการ panic ไม่ให้ Server ล้มเหลว
// Recovery — Handle panics to prevent server crashes
pub fn recovery(handler: Handler) -> Handler {
  fn(request: Request, context: Context, services: Services) -> Response {
    case erlang.rescue(fn() { handler(request, context, services) }) {
      Ok(response) -> response
      Error(error) -> {
        logger.error("Unhandled error: " <> string.inspect(error))
        dream_http.json_response(
          body: "{\"error\": \"Internal server error\"}",
          status: dream_http.internal_server_error,
        )
      }
    }
  }
}

4.3 การเขียน Custom Middleware

4.3.1 Authentication Middleware

// src/middleware.gleam

import gleam/string
import my_web_app/auth

// Auth middleware — ตรวจสอบ JWT token
// Auth middleware — Verify JWT token
pub fn auth_required(handler: Handler) -> Handler {
  fn(request: Request, context: Context, services: Services) -> Response {
    // อ่าน Authorization header
    // Read Authorization header
    case get_bearer_token(request) {
      Ok(token) ->
        case auth.verify_token(token, services.secret_key) {
          Ok(claims) ->
            // ใส่ user claims เข้าไปใน context
            // Inject user claims into context
            let authed_context = context.Context(
              ..context,
              user: Some(claims.user_id),
            )
            handler(request, authed_context, services)
          
          Error(_) ->
            dream_http.json_response(
              body: "{\"error\": \"Invalid token\"}",
              status: dream_http.unauthorized,
            )
        }
      
      Error(_) ->
        dream_http.json_response(
          body: "{\"error\": \"Missing authentication\"}",
          status: dream_http.unauthorized,
        )
    }
  }
}

// ดึง Bearer token จาก Authorization header
// Extract Bearer token from Authorization header
fn get_bearer_token(request: Request) -> Result(String, Nil) {
  request.headers
  |> list.key_find("authorization")
  |> result.try(fn(value) {
    case string.split_once(value, " ") {
      Ok(#("Bearer", token)) -> Ok(token)
      _ -> Error(Nil)
    }
  })
}

4.4 การประกอบแบบ Functional ด้วย Pipe Operator

4.4.1 Middleware Composition Pattern

// การใช้ |> เพื่อประกอบ Middleware
// Using |> to compose Middleware

// แบบที่ 1: กำหนด Middleware stack ใน Route
// Style 1: Define middleware stack in Route definition
|> router.route(
  method: http.Post,
  path: "/api/posts",
  controller: post_ctrl.create,
  middleware: [
    middleware.logger,           // 1. บันทึก log
    middleware.recovery,         // 2. จัดการ panic
    middleware.cors(allowed_origins: ["https://myapp.com"]),  // 3. CORS
    middleware.rate_limit(rpm: 60),                            // 4. Rate limit
    middleware.auth_required,    // 5. ตรวจสอบ Auth
    middleware.json_body,        // 6. Parse JSON body
  ],
)

// แบบที่ 2: สร้าง Middleware group
// Style 2: Create middleware groups
let api_middleware = [middleware.logger, middleware.recovery, middleware.cors_api]
let protected_middleware = list.append(api_middleware, [middleware.auth_required])
let admin_middleware = list.append(protected_middleware, [middleware.require_admin])

5. Services และ Dependency Injection (Services & Dependency Injection)

5.1 Services Pattern

Services Pattern คือการรวม dependencies ทั้งหมดของแอปพลิเคชันไว้ใน record เดียว แทนที่จะใช้ Global Variables หรือ Singleton

5.1.1 ทำไม Explicit DI ดีกว่า Global state

%%{init: {'theme': 'base', 'themeVariables': {
  'primaryColor': '#d65d0e',
  'primaryTextColor': '#ebdbb2',
  'primaryBorderColor': '#fe8019',
  'lineColor': '#fabd2f',
  'secondaryColor': '#3c3836',
  'background': '#282828',
  'mainBkg': '#3c3836',
  'clusterBkg': '#32302f',
  'titleColor': '#fe8019',
  'fontFamily': 'monospace'
}}}%%
graph LR
    subgraph BAD["❌ Global State (แย่)"]
        GDB["global_db = connect()"]
        GC["global_cache = Cache.new()"]
        CTRL1["Controller ใช้ global_db โดยตรง
ทดสอบยาก!"] GDB & GC --> CTRL1 end subgraph GOOD["✅ Services Pattern (ดี)"] SVCR["Services record
{db: Conn, cache: Cache}"] CTRL2["Controller รับ services
เป็น argument"] TEST["Test ใส่ Mock services
ทดสอบง่าย!"] SVCR --> CTRL2 TEST --> CTRL2 end style BAD fill:#3c3836,stroke:#fb4934 style GOOD fill:#3c3836,stroke:#b8bb26

5.2 การกำหนด Services Type

5.2.1 สร้าง Services Record

// src/services.gleam

import gleam_pgo as pgo
import my_web_app/repositories/user_repository.{type UserRepository}
import my_web_app/repositories/post_repository.{type PostRepository}
import my_web_app/cache.{type Cache}
import my_web_app/email.{type EmailService}

// กำหนด Type ของ Services ทั้งหมด
// Define the type for all application services
pub type Services {
  Services(
    db: pgo.Connection,               // Database connection pool
    cache: Cache,                     // In-memory cache
    user_repo: UserRepository,        // User repository
    post_repo: PostRepository,        // Post repository
    email: EmailService,              // Email service
    secret_key: String,               // JWT secret key
    config: AppConfig,                // Application configuration
  )
}

// กำหนด Type ของ Configuration
// Define application configuration type
pub type AppConfig {
  AppConfig(
    env: Environment,
    port: Int,
    database_url: String,
    redis_url: String,
    smtp_host: String,
  )
}

pub type Environment {
  Development
  Staging
  Production
}

// Factory function — สร้าง Services จาก environment
// Factory function — Create services from environment
pub fn create() -> Services {
  let config = load_config()
  let db = pgo.connect(pgo.Config(
    host: "localhost",
    port: 5432,
    database: "myapp",
    user: "postgres",
    password: option.Some("password"),
    pool_size: 10,
  ))
  let cache = cache.new(max_size: 1000, ttl_seconds: 300)
  
  Services(
    db:,
    cache:,
    user_repo: user_repository.new(db),
    post_repo: post_repository.new(db),
    email: email.new(config.smtp_host),
    secret_key: os.get_env("SECRET_KEY") |> result.unwrap("dev-secret"),
    config:,
  )
}

5.3 Context vs Services — ความแตกต่างสำคัญ

Services Context
อายุการใช้งาน ตลอดอายุแอป (Application lifetime) เฉพาะ Request นั้น (Per-request)
เนื้อหา DB, Cache, Config User info, Path params, Auth claims
การสร้าง ครั้งเดียวตอน startup สร้างใหม่ทุก Request
Mutability ไม่เปลี่ยนแปลง (Immutable) Middleware อาจแก้ไขได้
ตัวอย่าง services.db, services.email context.user_id, context.path_params

5.4 Testing ด้วย Services

5.4.1 Mock Services สำหรับ Unit Test

// test/helpers/mock_services.gleam

import my_web_app/services.{type Services}
import my_web_app/repositories/user_repository.{type UserRepository}

// สร้าง Mock UserRepository สำหรับการทดสอบ
// Create Mock UserRepository for testing
pub fn mock_user_repo(users: List(User)) -> UserRepository {
  UserRepository(
    // ส่งคืนข้อมูล users ที่กำหนดไว้ล่วงหน้า
    // Return predefined test users
    find_by_id: fn(id) {
      users
      |> list.find(fn(u) { u.id == id })
      |> result.map_error(fn(_) { user_repository.NotFound })
    },
    list: fn(page, limit) { 
      Ok(list.take(users, limit))
    },
    create: fn(attrs) { 
      Ok(User(id: "test-id-123", ..attrs))
    },
    delete: fn(_id) { Ok(Nil) },
  )
}

// สร้าง Test Services ทั้งหมด
// Create full test services
pub fn test_services(users: List(User)) -> Services {
  Services(
    db: pgo.test_connection(),          // Mock DB
    cache: cache.new_test(),            // In-memory test cache
    user_repo: mock_user_repo(users),   // Mock user repository
    post_repo: mock_post_repo([]),      // Empty post repository
    email: email.mock(),                // Mock email (ไม่ส่งจริง)
    secret_key: "test-secret-key",
    config: AppConfig(env: Development, port: 8080, ..),
  )
}

// ตัวอย่างการเขียน Test ด้วย Mock Services
// Example test using Mock Services
pub fn test_get_user_found() {
  let test_user = User(id: "user-1", name: "สมชาย", email: "somchai@test.com")
  let services = mock_services.test_services([test_user])
  let context = Context(path_params: dict.from_list([#("id", "user-1")]), user: None)
  let request = http.Request(method: http.Get, path: "/api/users/user-1", ..)
  
  let response = user_controller.get_user(request, context, services)
  
  // ตรวจสอบผลลัพธ์
  // Assert results
  assert response.status == 200
  assert string.contains(response.body, "สมชาย")
}

6. การจัดการข้อมูล (Handling Data)

6.1 JSON APIs

6.1.1 JSON Encode/Decode แบบ Type-safe

// src/models/user.gleam

import gleam_json as json
import gleam/dynamic.{type Dynamic}

// Model type สำหรับ User
// User model type
pub type User {
  User(
    id: String,
    name: String,
    email: String,
    role: UserRole,
    created_at: String,
  )
}

pub type UserRole {
  Admin
  Member
  Guest
}

// Encoder — แปลง User เป็น JSON
// Encoder — Convert User to JSON
pub fn to_json(user: User) -> json.Json {
  json.object([
    #("id",         json.string(user.id)),
    #("name",       json.string(user.name)),
    #("email",      json.string(user.email)),
    #("role",       json.string(role_to_string(user.role))),
    #("created_at", json.string(user.created_at)),
  ])
}

// Decoder — แปลง JSON เป็น User
// Decoder — Convert JSON to User
pub fn from_json(data: Dynamic) -> Result(User, List(dynamic.DecodeError)) {
  dynamic.decode5(
    User,
    dynamic.field("id",         dynamic.string),
    dynamic.field("name",       dynamic.string),
    dynamic.field("email",      dynamic.string),
    dynamic.field("role",       dynamic.string |> dynamic.map(string_to_role)),
    dynamic.field("created_at", dynamic.string),
  )(data)
}

fn role_to_string(role: UserRole) -> String {
  case role {
    Admin  -> "admin"
    Member -> "member"
    Guest  -> "guest"
  }
}

fn string_to_role(s: String) -> Result(UserRole, String) {
  case s {
    "admin"  -> Ok(Admin)
    "member" -> Ok(Member)
    "guest"  -> Ok(Guest)
    other    -> Error("Unknown role: " <> other)
  }
}

// ตัวอย่างการสร้าง JSON Response ใน Controller
// Example of creating JSON Response in Controller
pub fn list_users(request: Request, context: Context, services: Services) -> Response {
  let users = services.user_repo.list(page: 1, limit: 20)
  
  let json_body = json.object([
    #("data",  json.array(users, user.to_json)),
    #("total", json.int(list.length(users))),
  ]) |> json.to_string()
  
  dream_http.json_response(json_body, status: dream_http.ok)
}

6.2 Form Data และ URL-encoded Inputs

6.2.1 HTML Form Handling

// src/controllers/auth_controller.gleam

// Parse HTML form data (application/x-www-form-urlencoded)
// การ Parse ข้อมูลจาก HTML form
pub fn login(request: Request, context: Context, services: Services) -> Response {
  use form_data <- result.try(dream_http.read_form(request))
  
  let email    = form_data |> list.key_find("email")    |> result.unwrap("")
  let password = form_data |> list.key_find("password") |> result.unwrap("")
  
  case services.auth.verify(email, password) {
    Ok(user) -> {
      let token = auth.create_token(user.id, services.secret_key)
      dream_http.json_response(
        body: json.object([#("token", json.string(token))]) |> json.to_string(),
        status: dream_http.ok,
      )
    }
    Error(InvalidCredentials) ->
      dream_http.json_response(
        body: "{\"error\": \"Invalid email or password\"}",
        status: dream_http.unauthorized,
      )
  }
}

6.3 Input Validation

6.3.1 Custom Validators

// src/validation.gleam

// Validation result type
// ประเภทผลการตรวจสอบ
pub type ValidationError {
  Required(field: String)
  TooShort(field: String, min: Int)
  TooLong(field: String, max: Int)
  InvalidFormat(field: String, expected: String)
  InvalidValue(field: String, message: String)
}

pub type ValidationResult(a) = Result(a, List(ValidationError))

// ตรวจสอบ email format
// Validate email format
pub fn validate_email(value: String) -> ValidationResult(String) {
  case string.contains(value, "@") && string.length(value) > 5 {
    True  -> Ok(value)
    False -> Error([InvalidFormat("email", "must be a valid email address")])
  }
}

// ตรวจสอบ password strength
// Validate password strength
pub fn validate_password(value: String) -> ValidationResult(String) {
  let errors = []
  let errors = case string.length(value) < 8 {
    True  -> list.append(errors, [TooShort("password", 8)])
    False -> errors
  }
  case errors {
    [] -> Ok(value)
    _  -> Error(errors)
  }
}

// สร้าง CreateUserInput จาก JSON body
// Parse CreateUserInput from JSON body
pub type CreateUserInput {
  CreateUserInput(name: String, email: String, password: String)
}

pub fn validate_create_user(input: CreateUserInput) -> ValidationResult(CreateUserInput) {
  let name_result  = validate_required("name",  input.name)
  let email_result = validate_email(input.email)
  let pass_result  = validate_password(input.password)
  
  case name_result, email_result, pass_result {
    Ok(_), Ok(_), Ok(_) -> Ok(input)
    _, _, _ -> {
      let errors = [name_result, email_result, pass_result]
        |> list.filter_map(result.error)
        |> list.flatten
      Error(errors)
    }
  }
}

6.4 Response Builders

6.4.1 Helper Functions สำหรับสร้าง Responses

// src/response_helpers.gleam

// JSON response helper
pub fn json_ok(data: json.Json) -> Response {
  dream_http.json_response(json.to_string(data), status: dream_http.ok)
}

pub fn json_created(data: json.Json) -> Response {
  dream_http.json_response(json.to_string(data), status: dream_http.created)
}

pub fn json_error(message: String, status: Int) -> Response {
  let body = json.object([#("error", json.string(message))]) |> json.to_string()
  dream_http.json_response(body, status:)
}

// Redirect helper
pub fn redirect_to(path: String) -> Response {
  Response(
    status: dream_http.found,
    headers: [#("Location", path)],
    body: dream_http.Empty,
  )
}

// Validation error response
pub fn validation_error_response(errors: List(ValidationError)) -> Response {
  let error_messages = errors |> list.map(validation_error_to_string)
  let body = json.object([
    #("error",  json.string("Validation failed")),
    #("errors", json.array(error_messages, json.string)),
  ]) |> json.to_string()
  dream_http.json_response(body, status: dream_http.bad_request)
}

7. Streaming และ Real-time (Streaming & Real-time)

7.1 Streaming Responses

7.1.1 ส่งข้อมูลขนาดใหญ่แบบ Stream

// src/controllers/export_controller.gleam

// ส่งข้อมูล CSV ขนาดใหญ่แบบ streaming
// Stream large CSV data without loading all into memory
pub fn export_users_csv(
  _request: Request,
  _context: Context,
  services: Services,
) -> Response {
  // สร้าง stream ที่ดึงข้อมูลทีละชุด
  // Create a stream that fetches data in chunks
  let stream = stream.new(fn(emit) {
    // ส่ง CSV header ก่อน
    // Emit CSV header first
    emit("id,name,email,created_at\n")
    
    // วนดึงข้อมูลทีละ 1000 รายการ
    // Fetch 1000 records at a time
    let _ = services.user_repo.stream_all(fn(user) {
      emit(
        user.id <> "," <>
        user.name <> "," <>
        user.email <> "," <>
        user.created_at <> "\n"
      )
    })
  })
  
  dream_http.stream_response(
    stream:,
    status: dream_http.ok,
    headers: [
      #("Content-Type",        "text/csv; charset=utf-8"),
      #("Content-Disposition", "attachment; filename=\"users.csv\""),
    ],
  )
}

7.2 WebSocket Fundamentals

7.2.1 Upgrade HTTP เป็น WebSocket

// src/controllers/chat_controller.gleam

import dream/websocket

// กำหนด Type สำหรับ Chat Room state
// Define Chat Room state type
pub type ChatState {
  ChatState(
    room_id: String,
    username: String,
    connections: List(websocket.Connection),
  )
}

// Upgrade HTTP request เป็น WebSocket connection
// Upgrade HTTP request to WebSocket connection
pub fn chat_socket(
  request: Request,
  context: Context,
  services: Services,
) -> Response {
  let room_id = context.path_params |> dict.get("room_id") |> result.unwrap("")
  let username = context.user |> option.unwrap("anonymous")
  
  // เพิ่ม dependencies ที่ต้องการเข้าไปใน initial state
  // Inject required dependencies into initial state
  let initial_state = ChatState(room_id:, username:, connections: [])
  
  websocket.upgrade_websocket(
    request:,
    initial_state:,
    on_init:    chat_on_init,
    on_message: chat_on_message,
    on_close:   chat_on_close,
  )
}

7.3 WebSocket Lifecycle

7.3.1 Event Handlers ครบวงจร

// WebSocket lifecycle handlers

// เรียกเมื่อ client เชื่อมต่อสำเร็จ
// Called when client successfully connects
fn chat_on_init(conn: websocket.Connection, state: ChatState) -> ChatState {
  // แจ้ง room ว่ามี user เข้ามาใหม่
  // Notify room that a new user has joined
  let welcome_msg = json.object([
    #("type",     json.string("system")),
    #("message",  json.string(state.username <> " เข้าร่วมห้องสนทนา")),
    #("room",     json.string(state.room_id)),
  ]) |> json.to_string()
  
  websocket.broadcast_to_room(state.room_id, welcome_msg)
  
  // เพิ่ม connection เข้าไปใน state
  // Add connection to state
  ChatState(..state, connections: [conn, ..state.connections])
}

// เรียกเมื่อได้รับ message จาก client
// Called when message received from client  
fn chat_on_message(
  _conn: websocket.Connection,
  message: websocket.Message,
  state: ChatState,
) -> #(websocket.Response, ChatState) {
  case message {
    websocket.Text(text) -> {
      // Broadcast ข้อความไปยัง users ทุกคนในห้อง
      // Broadcast message to all users in the room
      let chat_msg = json.object([
        #("type",     json.string("chat")),
        #("from",     json.string(state.username)),
        #("message",  json.string(text)),
        #("room",     json.string(state.room_id)),
      ]) |> json.to_string()
      
      websocket.broadcast_to_room(state.room_id, chat_msg)
      #(websocket.Continue, state)
    }
    
    websocket.Binary(_) -> #(websocket.Continue, state)
    websocket.Close ->     #(websocket.Close,    state)
  }
}

// เรียกเมื่อ client ตัดการเชื่อมต่อ
// Called when client disconnects
fn chat_on_close(_conn: websocket.Connection, state: ChatState) -> Nil {
  let leave_msg = state.username <> " ออกจากห้องสนทนา"
  websocket.broadcast_to_room(state.room_id, leave_msg)
}

7.4 Server-Sent Events (SSE)

7.4.1 Real-time Updates แบบ Unidirectional

// src/controllers/notification_controller.gleam

// SSE endpoint สำหรับ Real-time notifications
// SSE endpoint for real-time notifications
pub fn notifications_stream(
  request: Request,
  context: Context,
  services: Services,
) -> Response {
  let user_id = context.user |> option.unwrap("anonymous")
  
  let sse_stream = stream.new(fn(emit) {
    // ส่ง initial connection message
    // Send initial connection confirmation
    emit("event: connected\ndata: {\"status\":\"connected\"}\n\n")
    
    // Subscribe to notifications for this user
    let _subscription = services.notification_service.subscribe(
      user_id:,
      on_notification: fn(notification) {
        let data = json.object([
          #("id",       json.string(notification.id)),
          #("type",     json.string(notification.type_)),
          #("message",  json.string(notification.message)),
        ]) |> json.to_string()
        
        // SSE format: "event: <type>\ndata: <json>\n\n"
        emit("event: notification\ndata: " <> data <> "\n\n")
      },
    )
    
    // Keep connection alive ด้วย heartbeat ทุก 30 วินาที
    // Keep connection alive with 30-second heartbeat
    timer.repeat(30_000, fn() { emit(": heartbeat\n\n") })
  })
  
  dream_http.stream_response(
    stream: sse_stream,
    status: dream_http.ok,
    headers: [
      #("Content-Type",  "text/event-stream"),
      #("Cache-Control", "no-cache"),
      #("Connection",    "keep-alive"),
    ],
  )
}

8. Application State และ Concurrency (Application State & Concurrency)

8.1 Stateless Handlers — ค่าเริ่มต้นที่ดีที่สุด

Pure function handlers ทำให้โปรแกรมทดสอบง่าย ปรับขนาดได้ และคาดเดาผลได้

// Stateless handler — pure function, ไม่มี side effects นอกจากผ่าน services
// Stateless handler — pure function, side effects only through services
pub fn calculate_discount(
  request: Request,
  _context: Context,
  _services: Services,  // ไม่ต้องการ services เลย
) -> Response {
  case decode_discount_request(request) {
    Ok(input) -> {
      // คำนวณส่วนลด — pure function
      // Calculate discount — pure function
      let discount = input.price *. float.from_int(input.discount_percent) /. 100.0
      let final_price = input.price -. discount
      
      json_ok(json.object([
        #("original_price", json.float(input.price)),
        #("discount",       json.float(discount)),
        #("final_price",    json.float(final_price)),
      ]))
    }
    Error(msg) -> json_error(msg, dream_http.bad_request)
  }
}

8.2 การจัดการ State ด้วย OTP Actors

8.2.1 Counter Actor ด้วย gleam/otp

// src/actors/visit_counter.gleam

import gleam/otp/actor.{type StartError}
import gleam/erlang/process.{type Subject}

// Messages ที่ Actor รับได้
// Messages the actor can receive
pub type CounterMessage {
  Increment(path: String)
  GetCount(path: String, reply_to: Subject(Int))
  Reset
}

// State ของ Actor
// Actor's internal state
pub type CounterState {
  CounterState(counts: dict.Dict(String, Int))
}

// เริ่มต้น Actor
// Start the actor
pub fn start() -> Result(Subject(CounterMessage), StartError) {
  actor.start(
    CounterState(counts: dict.new()),
    handle_message,
  )
}

// จัดการ messages
// Handle messages
fn handle_message(
  message: CounterMessage,
  state: CounterState,
) -> actor.Next(CounterMessage, CounterState) {
  case message {
    Increment(path) -> {
      let new_count = state.counts |> dict.get(path) |> result.unwrap(0) |> int.add(1)
      let new_counts = dict.insert(state.counts, path, new_count)
      actor.continue(CounterState(counts: new_counts))
    }
    
    GetCount(path, reply_to) -> {
      let count = state.counts |> dict.get(path) |> result.unwrap(0)
      process.send(reply_to, count)
      actor.continue(state)
    }
    
    Reset ->
      actor.continue(CounterState(counts: dict.new()))
  }
}

8.3 Caching Patterns

8.3.1 In-memory Cache ด้วย ETS

// src/cache.gleam

// ใช้ Erlang ETS (Erlang Term Storage) สำหรับ In-memory cache
// Use Erlang ETS for In-memory cache
import gleam/erlang/atom
import gleam_erlang/ets

pub type Cache {
  Cache(table: ets.Table, ttl_seconds: Int)
}

// สร้าง Cache table
// Create Cache table
pub fn new(name: String, ttl_seconds: Int) -> Cache {
  let table = ets.new(atom.create_from_string(name), [
    ets.Set,
    ets.Public,
    ets.NamedTable,
  ])
  Cache(table:, ttl_seconds:)
}

// เก็บค่าใน Cache พร้อม TTL
// Store value in Cache with TTL
pub fn set(cache: Cache, key: String, value: a) -> Nil {
  let expires_at = erlang.system_time(erlang.Second) + cache.ttl_seconds
  ets.insert(cache.table, #(key, value, expires_at))
}

// ดึงค่าจาก Cache (ตรวจสอบ TTL)
// Get value from Cache (with TTL check)
pub fn get(cache: Cache, key: String) -> Option(a) {
  case ets.lookup(cache.table, key) {
    [#(_key, value, expires_at)] -> {
      let now = erlang.system_time(erlang.Second)
      case now < expires_at {
        True  -> Some(value)
        False -> {
          ets.delete(cache.table, key)
          None
        }
      }
    }
    _ -> None
  }
}

8.4 Background Jobs

8.4.1 Spawning Processes สำหรับงานเบื้องหลัง

// src/jobs/email_job.gleam

import gleam/erlang/process

// ส่ง email แบบ asynchronous (ไม่บล็อก Request)
// Send email asynchronously (non-blocking)
pub fn send_welcome_email_async(user: User, services: Services) -> Nil {
  // spawn process แยกต่างหาก
  // Spawn a separate process
  process.start(
    fn() {
      // ทำงานใน background — ไม่กระทบ Request หลัก
      // Run in background — doesn't affect main request
      case services.email.send_welcome(user) {
        Ok(_) -> logger.info("Welcome email sent to " <> user.email)
        Error(err) -> logger.error("Failed to send email: " <> string.inspect(err))
      }
    },
    linked: False,  // ไม่ link กับ parent process — crash ไม่ลามไป
  )
}

// ใช้งานใน Controller
// Usage in Controller
pub fn register(request: Request, _context: Context, services: Services) -> Response {
  case decode_register_input(request) {
    Ok(input) ->
      case services.user_repo.create(input) {
        Ok(user) -> {
          // ส่ง email ใน background ทันที ไม่ต้องรอ
          // Send email in background immediately, no waiting
          email_job.send_welcome_email_async(user, services)
          json_created(user.to_json(user))
        }
        Error(err) -> json_error(string.inspect(err), 500)
      }
    Error(msg) -> json_error(msg, 400)
  }
}

9. Database Integration

9.1 เชื่อมต่อ PostgreSQL ด้วย gleam_pgo

9.1.1 Connection Pool Configuration

// src/database.gleam

import gleam_pgo as pgo

// กำหนดค่า Connection Pool
// Configure Connection Pool
pub fn connect(config: AppConfig) -> pgo.Connection {
  pgo.connect(
    pgo.Config(
      host:      config.db_host,
      port:      config.db_port,
      database:  config.db_name,
      user:      config.db_user,
      password:  option.Some(config.db_password),
      ssl:       config.env == Production,
      // Pool settings — จำนวน connections พร้อมกัน
      // Pool settings — concurrent connections count
      pool_size: case config.env {
        Production  -> 20
        Staging     -> 10
        Development -> 5
      },
      // Timeout สำหรับ query
      // Query timeout
      queue_target: 50,     // ms — เริ่ม queue เมื่อช้ากว่า 50ms
      queue_interval: 1000, // ms — ตรวจสอบ queue ทุก 1 วินาที
    )
  )
}

9.2 Type-safe Queries

9.2.1 Parameterized Queries และ Row Decoding

// src/repositories/user_repository.gleam

import gleam_pgo as pgo
import gleam/dynamic

pub type UserRepository {
  UserRepository(
    find_by_id: fn(String) -> Result(User, UserError),
    list:       fn(Int, Int) -> Result(List(User), UserError),
    create:     fn(CreateUserInput) -> Result(User, UserError),
    update:     fn(String, UpdateUserInput) -> Result(User, UserError),
    delete:     fn(String) -> Result(Nil, UserError),
  )
}

pub type UserError {
  NotFound
  DuplicateEmail
  DatabaseError(String)
}

// สร้าง Repository จาก Database connection
// Create Repository from Database connection
pub fn new(db: pgo.Connection) -> UserRepository {
  UserRepository(
    find_by_id: find_by_id(db, _),
    list:       list(db, _, _),
    create:     create(db, _),
    update:     update(db, _, _),
    delete:     delete(db, _),
  )
}

// Query ดึง User ด้วย ID
// Query to fetch User by ID
fn find_by_id(db: pgo.Connection, id: String) -> Result(User, UserError) {
  // Parameterized query — ป้องกัน SQL Injection
  // Parameterized query — prevents SQL Injection
  let sql = "SELECT id, name, email, role, created_at FROM users WHERE id = $1"
  
  pgo.execute(
    sql,
    db,
    [pgo.text(id)],   // Parameters แยกจาก SQL — ปลอดภัยจาก Injection
    decode_user,       // Row decoder
  )
  |> result.map_error(fn(e) { DatabaseError(string.inspect(e)) })
  |> result.try(fn(rows) {
    case rows {
      [user] -> Ok(user)
      []     -> Error(NotFound)
      _      -> Error(DatabaseError("Multiple rows returned for single ID"))
    }
  })
}

// Decoder สำหรับแปลง Database Row เป็น User type
// Decoder to convert Database Row to User type
fn decode_user(row: dynamic.Dynamic) -> Result(User, List(dynamic.DecodeError)) {
  dynamic.decode5(
    User,
    dynamic.element(0, dynamic.string),     // id
    dynamic.element(1, dynamic.string),     // name
    dynamic.element(2, dynamic.string),     // email
    dynamic.element(3, dynamic.string |> dynamic.map(string_to_role)),  // role
    dynamic.element(4, dynamic.string),     // created_at
  )(row)
}

9.3 Repository Pattern

9.3.1 แยก DB Logic ออกจาก Controller

%%{init: {'theme': 'base', 'themeVariables': {
  'primaryColor': '#689d6a',
  'primaryTextColor': '#ebdbb2',
  'primaryBorderColor': '#8ec07c',
  'lineColor': '#fabd2f',
  'secondaryColor': '#3c3836',
  'background': '#282828',
  'mainBkg': '#3c3836',
  'clusterBkg': '#32302f',
  'titleColor': '#8ec07c',
  'fontFamily': 'monospace'
}}}%%
graph LR
    HTTP["HTTP Request"] --> CTRL["Controller\nตรวจสอบ input\nสร้าง response"]
    CTRL --> REPO["Repository\nDB queries\nRow decoding"]
    REPO --> PG["PostgreSQL\nDatabase"]
    REPO --> CTRL
    CTRL --> HTTP

    subgraph SVC["Services Layer"]
        REPO
    end

    style CTRL fill:#458588,stroke:#83a598,color:#ebdbb2
    style REPO fill:#98971a,stroke:#b8bb26,color:#ebdbb2
    style PG fill:#3c3836,stroke:#fabd2f,color:#fabd2f
    style SVC fill:#32302f,stroke:#504945

9.4 Schema Migrations

9.4.1 SQL Migration Files

-- migrations/001_create_users.sql
-- Migration ที่ 1: สร้างตาราง users
-- Migration 1: Create users table

CREATE TABLE users (
    id         UUID        PRIMARY KEY DEFAULT gen_random_uuid(),
    name       VARCHAR(100) NOT NULL,
    email      VARCHAR(255) NOT NULL UNIQUE,
    role       VARCHAR(20)  NOT NULL DEFAULT 'member',
    password_hash VARCHAR(255) NOT NULL,
    created_at TIMESTAMPTZ  NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ  NOT NULL DEFAULT NOW()
);

-- Index สำหรับ email lookup ที่ใช้บ่อย
-- Index for frequent email lookups
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_role  ON users(role);

-- migrations/002_create_posts.sql
CREATE TABLE posts (
    id         UUID         PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id    UUID         NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    title      VARCHAR(500) NOT NULL,
    body       TEXT         NOT NULL,
    published  BOOLEAN      NOT NULL DEFAULT FALSE,
    created_at TIMESTAMPTZ  NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_posts_user_id   ON posts(user_id);
CREATE INDEX idx_posts_published ON posts(published);

10. Templates และ Frontend (Templates & Frontend)

10.1 การสร้าง HTML — DSL vs Template Engine

10.1.1 Nakai HTML DSL

// src/views/user_view.gleam

import nakai/html.{type Node}
import nakai/attr

// สร้าง HTML ด้วย Type-safe DSL
// Create HTML with Type-safe DSL
pub fn user_list_page(users: List(User)) -> Node {
  html.html([attr.lang("th")], [
    html.head([], [
      html.meta([attr.charset("UTF-8")]),
      html.title([], "รายการผู้ใช้งาน"),
      html.link([attr.rel("stylesheet"), attr.href("/static/app.css")]),
    ]),
    html.body([attr.class("container")], [
      html.h1([], [html.Text("รายการผู้ใช้งาน")]),
      html.p([], [html.Text("พบผู้ใช้งาน " <> int.to_string(list.length(users)) <> " รายการ")]),
      
      // Render รายการ users
      // Render user list
      html.ul([attr.class("user-list")],
        list.map(users, render_user_item)
      ),
    ]),
  ])
}

// Render แต่ละ user item
// Render individual user item
fn render_user_item(user: User) -> Node {
  html.li([attr.class("user-item")], [
    html.a([attr.href("/users/" <> user.id)], [
      html.span([attr.class("user-name")], [html.Text(user.name)]),
      html.span([attr.class("user-email")], [html.Text(user.email)]),
    ]),
  ])
}

// ใช้ใน Controller
// Use in Controller
pub fn index(request: Request, _context: Context, services: Services) -> Response {
  let users = services.user_repo.list(page: 1, limit: 50) |> result.unwrap([])
  let html_string = user_view.user_list_page(users) |> nakai.to_string()
  dream_http.html_response(html_string, status: dream_http.ok)
}

10.2 การ Serve Static Assets

10.2.1 Static File Middleware

// src/router.gleam

pub fn router() -> Router {
  router.new()
  // Serve static files จาก priv/static/
  // Serve static files from priv/static/
  |> router.static(
    path: "/static",
    directory: "priv/static",
  )
  // ไฟล์ Structure:
  // priv/static/
  // ├── app.css
  // ├── app.js
  // └── images/
  //     └── logo.png
  
  |> router.route(method: http.Get, path: "/", controller: home_ctrl.index, middleware: [])
  // ... other routes
}

10.3 การใช้งานร่วมกับ Lustre

10.3.1 Server-side Rendering + Client-side Interactivity

// Lustre ใช้ Elm Architecture (Model-Update-View)
// Lustre uses Elm Architecture (Model-Update-View)

// คือ Frontend framework สำหรับ Gleam
// It's a Frontend framework for Gleam

// Model — State ของ UI
pub type Model {
  Model(count: Int, loading: Bool)
}

// Msg — Events จาก UI
pub type Msg {
  Increment
  Decrement
  Reset
}

// Update — สร้าง Model ใหม่จาก Message
pub fn update(model: Model, msg: Msg) -> #(Model, lustre.Effect(Msg)) {
  case msg {
    Increment -> #(Model(..model, count: model.count + 1), lustre.none())
    Decrement -> #(Model(..model, count: model.count - 1), lustre.none())
    Reset     -> #(Model(count: 0, loading: False), lustre.none())
  }
}

// View — แปลง Model เป็น HTML
pub fn view(model: Model) -> lustre.Element(Msg) {
  html.div([attr.class("counter")], [
    html.button([event.on_click(Decrement)], [html.text("-")]),
    html.span([], [html.text(int.to_string(model.count))]),
    html.button([event.on_click(Increment)], [html.text("+")]),
  ])
}

10.4 HTMX Integration

10.4.1 Hypermedia-driven UI

// HTMX ช่วยให้ HTML tags ส่ง AJAX requests ได้โดยไม่ต้องเขียน JavaScript
// HTMX allows HTML tags to make AJAX requests without writing JavaScript

// ฝั่ง Server — Controller ที่ตอบสนอง HTMX
// Server side — HTMX-aware Controller
pub fn search_users(request: Request, _context: Context, services: Services) -> Response {
  let query = get_query_param(request, "q") |> result.unwrap("")
  let users = services.user_repo.search(query) |> result.unwrap([])
  
  // ตรวจสอบว่าเป็น HTMX request หรือไม่
  // Check if this is an HTMX request
  let is_htmx = request.headers
    |> list.key_find("hx-request")
    |> result.is_ok()
  
  case is_htmx {
    // ส่งกลับเฉพาะ HTML fragment สำหรับ HTMX
    // Return only HTML fragment for HTMX
    True -> {
      let fragment = render_user_list_fragment(users) |> nakai.to_string()
      dream_http.html_response(fragment, status: dream_http.ok)
    }
    // ส่งกลับ full page สำหรับ regular request
    // Return full page for regular request
    False -> {
      let page = user_view.user_list_page(users) |> nakai.to_string()
      dream_http.html_response(page, status: dream_http.ok)
    }
  }
}
<!-- ฝั่ง Frontend — HTMX attributes -->
<!-- Frontend side — HTMX attributes -->
<input
  type="search"
  name="q"
  placeholder="ค้นหาผู้ใช้..."
  hx-get="/api/users/search"
  hx-trigger="keyup changed delay:300ms"
  hx-target="#user-results"
  hx-indicator="#loading-spinner"
/>

<div id="loading-spinner" class="htmx-indicator">กำลังค้นหา...</div>
<div id="user-results"><!-- ผลการค้นหาจะแสดงที่นี่ --></div>

11. Error Handling

11.1 Type-safe Errors ด้วย Result

11.1.1 Result Type และ Error Propagation

// ตัวอย่างการใช้ Result สำหรับ Error propagation
// Example of using Result for Error propagation

// ไม่ใช่ Exception — เป็น Type!
// Not exceptions — they're types!
pub type AppError {
  NotFound(resource: String, id: String)
  Unauthorized(message: String)
  Forbidden(action: String)
  Validation(errors: List(ValidationError))
  Database(message: String)
  External(service: String, message: String)
}

// การใช้ use expression เพื่อ chain operations อย่างสะอาด
// Using use expression to chain operations cleanly
pub fn transfer_funds(
  from_id: String,
  to_id: String,
  amount: Float,
  services: Services,
) -> Result(Transfer, AppError) {
  // use คือ syntactic sugar สำหรับ Result.try
  // use is syntactic sugar for Result.try
  use from_account <- result.try(
    services.account_repo.find_by_id(from_id)
    |> result.map_error(fn(_) { NotFound("account", from_id) })
  )
  
  use to_account <- result.try(
    services.account_repo.find_by_id(to_id)
    |> result.map_error(fn(_) { NotFound("account", to_id) })
  )
  
  use _ <- result.try(
    case from_account.balance >=. amount {
      True  -> Ok(Nil)
      False -> Error(Forbidden("Insufficient balance"))
    }
  )
  
  services.account_repo.transfer(from_account, to_account, amount)
  |> result.map_error(fn(e) { Database(string.inspect(e)) })
}

11.2 Custom Domain Error Types

11.2.1 Mapping Domain Errors เป็น HTTP Responses

// src/error_handler.gleam

// แปลง Domain Error เป็น HTTP Response
// Convert Domain Error to HTTP Response
pub fn to_response(error: AppError) -> Response {
  case error {
    NotFound(resource, id) ->
      json_error(
        resource <> " '" <> id <> "' ไม่พบ",
        dream_http.not_found,
      )
    
    Unauthorized(message) ->
      json_error(message, dream_http.unauthorized)
    
    Forbidden(action) ->
      json_error("ไม่มีสิทธิ์: " <> action, dream_http.forbidden)
    
    Validation(errors) ->
      validation_error_response(errors)
    
    Database(message) -> {
      logger.error("Database error: " <> message)
      json_error("เกิดข้อผิดพลาดภายใน", dream_http.internal_server_error)
    }
    
    External(service, message) -> {
      logger.error("External service error [" <> service <> "]: " <> message)
      json_error("บริการ " <> service <> " ไม่พร้อมใช้งาน", dream_http.service_unavailable)
    }
  }
}

// ใช้ใน Controller ด้วย result.map_error
// Use in Controller with result.map_error
pub fn create_user(request: Request, _context: Context, services: Services) -> Response {
  decode_create_user_input(request)
  |> result.map_error(fn(errors) { app_error.Validation(errors) })
  |> result.try(services.user_repo.create)
  |> result.map(fn(user) { json_created(user.to_json(user)) })
  |> result.map_error(error_handler.to_response)
  |> result.unwrap_both()
}

11.3 Recovery Middleware

// สร้าง Error page ที่เป็นมิตรกับผู้ใช้
// Create user-friendly error pages

pub fn custom_recovery(handler: Handler) -> Handler {
  fn(request: Request, context: Context, services: Services) -> Response {
    case erlang.rescue(fn() { handler(request, context, services) }) {
      Ok(response) -> response
      Error(error) -> {
        logger.error("Unhandled panic: " <> string.inspect(error))
        
        // ส่ง JSON สำหรับ API requests
        // Send JSON for API requests
        case is_api_request(request) {
          True ->
            json_error("เกิดข้อผิดพลาดที่ไม่คาดคิด", dream_http.internal_server_error)
          
          // ส่ง HTML error page สำหรับ browser requests
          // Send HTML error page for browser requests
          False ->
            dream_http.html_response(
              render_500_page(),
              status: dream_http.internal_server_error,
            )
        }
      }
    }
  }
}

11.4 Structured Logging

11.4.1 JSON Logging สำหรับ Production

// src/logger.gleam

pub type LogLevel { Debug | Info | Warn | Error }

// บันทึก log แบบ JSON structured
// JSON structured logging
pub fn log(level: LogLevel, message: String, fields: List(#(String, json.Json))) -> Nil {
  let entry = json.object(list.flatten([
    [
      #("timestamp", json.string(erlang.system_time_to_string())),
      #("level",     json.string(level_to_string(level))),
      #("message",   json.string(message)),
    ],
    fields,
  ])) |> json.to_string()
  
  io.println(entry)
}

// ตัวอย่างการใช้งาน
// Usage examples
pub fn log_request(request: Request, response: Response, duration_ms: Int) -> Nil {
  log(Info, "HTTP Request", [
    #("method",   json.string(http.method_to_string(request.method))),
    #("path",     json.string(request.path)),
    #("status",   json.int(response.status)),
    #("duration", json.int(duration_ms)),
  ])
}

12. Security

12.1 CORS Configuration

12.1.1 Origin Policies และ Preflight Handling

// src/middleware/cors.gleam

pub type CorsConfig {
  CorsConfig(
    allowed_origins: List(String),
    allowed_methods: List(http.Method),
    allowed_headers: List(String),
    allow_credentials: Bool,
    max_age: Int,
  )
}

// CORS สำหรับ REST API
// CORS for REST API
pub fn api_cors_config() -> CorsConfig {
  CorsConfig(
    allowed_origins:   ["https://myapp.com", "https://www.myapp.com"],
    allowed_methods:   [http.Get, http.Post, http.Put, http.Delete, http.Options],
    allowed_headers:   ["Content-Type", "Authorization", "X-Request-ID"],
    allow_credentials: True,
    max_age:           86400,  // 24 ชั่วโมง
  )
}

pub fn cors(config: CorsConfig) -> Middleware {
  fn(handler: Handler) -> Handler {
    fn(request: Request, context: Context, services: Services) -> Response {
      let origin = request.headers |> list.key_find("origin") |> result.unwrap("")
      
      // Preflight request (OPTIONS)
      case request.method == http.Options {
        True ->
          Response(
            status:  dream_http.no_content,
            headers: cors_headers(config, origin),
            body:    dream_http.Empty,
          )
        
        False -> {
          let response = handler(request, context, services)
          Response(
            ..response,
            headers: list.append(response.headers, cors_headers(config, origin)),
          )
        }
      }
    }
  }
}

12.2 CSRF Protection

12.2.1 Token-based CSRF

// CSRF protection สำหรับ Form-based applications
// CSRF protection for Form-based applications

pub fn csrf_middleware(handler: Handler) -> Handler {
  fn(request: Request, context: Context, services: Services) -> Response {
    // ข้าม CSRF check สำหรับ GET, HEAD, OPTIONS (safe methods)
    // Skip CSRF check for safe methods
    case request.method {
      http.Get | http.Head | http.Options ->
        handler(request, context, services)
      
      _ -> {
        // ตรวจสอบ CSRF token จาก header หรือ form field
        // Verify CSRF token from header or form field
        let token_valid =
          get_csrf_token_from_header(request)
          |> result.or(get_csrf_token_from_form(request))
          |> result.map(fn(token) {
            timing_safe_compare(token, context.csrf_token)
          })
          |> result.unwrap(False)
        
        case token_valid {
          True  -> handler(request, context, services)
          False ->
            json_error("CSRF token invalid", dream_http.forbidden)
        }
      }
    }
  }
}

12.3 Rate Limiting

12.3.1 Token Bucket Algorithm

Token Bucket ทำงานบนสูตร:

tokenst+1 = min ( capacitymax , tokenst + refillrate × Δt )

โดยที่:

ตัวอย่างการคำนวณ:

สมมติว่า capacity = 10 requests, refill_rate = 2 token/วินาที, ปัจจุบัน tokens = 3, Δt = 2.5 วินาที

tokensnew = min ( 10 , 3 + 2 × 2.5 ) = min ( 10 , 8 ) = 8 tokens
// src/rate_limiter.gleam

pub type BucketState {
  BucketState(tokens: Float, last_refill: Int)
}

pub type RateLimitConfig {
  RateLimitConfig(
    capacity: Float,       // ความจุสูงสุด
    refill_rate: Float,    // token ต่อวินาที
  )
}

// ตรวจสอบและอัปเดต token bucket
// Check and update token bucket
pub fn check_and_consume(
  bucket: BucketState,
  config: RateLimitConfig,
) -> #(Bool, BucketState) {
  let now = erlang.system_time(erlang.Millisecond)
  let elapsed_seconds = int.to_float(now - bucket.last_refill) /. 1000.0
  
  // คำนวณ tokens ใหม่ตามสูตร Token Bucket
  // Calculate new tokens using Token Bucket formula
  let refilled_tokens = float.min(
    config.capacity,
    bucket.tokens +. config.refill_rate *. elapsed_seconds,
  )
  
  case refilled_tokens >=. 1.0 {
    True -> {
      // อนุญาต request — ใช้ 1 token
      // Allow request — consume 1 token
      let new_bucket = BucketState(
        tokens:      refilled_tokens -. 1.0,
        last_refill: now,
      )
      #(True, new_bucket)
    }
    False ->
      // ปฏิเสธ request — ไม่มี token เพียงพอ
      // Deny request — insufficient tokens
      #(False, BucketState(tokens: refilled_tokens, last_refill: now))
  }
}

12.4 Security Headers

12.4.1 Security Headers Middleware

// src/middleware/security_headers.gleam

// เพิ่ม Security headers สำหรับ Production
// Add Security headers for Production
pub fn security_headers(handler: Handler) -> Handler {
  fn(request: Request, context: Context, services: Services) -> Response {
    let response = handler(request, context, services)
    Response(
      ..response,
      headers: list.append(response.headers, [
        // ป้องกัน XSS
        // Prevent XSS
        #("Content-Security-Policy",
          "default-src 'self'; script-src 'self' 'nonce-{nonce}'; style-src 'self'"),
        
        // บังคับใช้ HTTPS
        // Enforce HTTPS
        #("Strict-Transport-Security",
          "max-age=31536000; includeSubDomains; preload"),
        
        // ป้องกัน Clickjacking
        // Prevent Clickjacking
        #("X-Frame-Options", "DENY"),
        
        // ป้องกัน MIME sniffing
        // Prevent MIME sniffing
        #("X-Content-Type-Options", "nosniff"),
        
        // Referrer Policy
        #("Referrer-Policy", "strict-origin-when-cross-origin"),
        
        // Permissions Policy
        #("Permissions-Policy", "camera=(), microphone=(), geolocation=()"),
      ]),
    )
  }
}

12.5 Secrets Management

12.5.1 Environment Variables

// src/config.gleam

import gleam/os

// โหลด config จาก environment variables
// Load config from environment variables
pub fn load() -> Result(AppConfig, String) {
  use db_url <- result.try(
    os.get_env("DATABASE_URL")
    |> result.map_error(fn(_) { "DATABASE_URL is required" })
  )
  
  use secret_key <- result.try(
    os.get_env("SECRET_KEY")
    |> result.map_error(fn(_) { "SECRET_KEY is required" })
  )
  
  let port = os.get_env("PORT")
    |> result.try(int.parse)
    |> result.unwrap(8080)
  
  Ok(AppConfig(
    database_url: db_url,
    secret_key: secret_key,
    port: port,
    env: detect_environment(),
  ))
}

fn detect_environment() -> Environment {
  case os.get_env("APP_ENV") {
    Ok("production") -> Production
    Ok("staging")    -> Staging
    _                -> Development
  }
}
# .env.example — Template สำหรับ environment variables
# ห้าม commit ไฟล์ .env จริงเข้า Git!
# Never commit actual .env files to Git!

DATABASE_URL=postgres://user:password@localhost:5432/myapp
SECRET_KEY=your-super-secret-key-at-least-32-chars
PORT=8080
APP_ENV=development
SMTP_HOST=smtp.sendgrid.net
REDIS_URL=redis://localhost:6379

13. Testing

13.1 Unit Testing Controllers ด้วย gleeunit

13.1.1 โครงสร้าง Test File

// test/controllers/user_controller_test.gleam

import gleeunit
import gleeunit/should
import my_web_app/controllers/user_controller
import test/helpers/mock_services.{test_services}
import test/helpers/mock_context.{test_context}
import test/helpers/request_builder.{get_request, post_request_with_body}

// gleeunit entry point
pub fn main() { gleeunit.main() }

// ทดสอบ get_user เมื่อพบ User
// Test get_user when user exists
pub fn get_user_returns_200_when_found_test() {
  // Arrange — เตรียมข้อมูลทดสอบ
  // Arrange — prepare test data
  let user = User(id: "user-1", name: "สมชาย ใจดี", email: "somchai@test.com", role: Member, created_at: "2024-01-01")
  let services = test_services(users: [user])
  let context  = test_context(path_params: [#("id", "user-1")])
  let request  = get_request("/api/users/user-1")
  
  // Act — ทดสอบ function จริง
  // Act — call the actual function
  let response = user_controller.get_user(request, context, services)
  
  // Assert — ตรวจสอบผลลัพธ์
  // Assert — verify results
  response.status |> should.equal(200)
  response.body   |> should.contain("สมชาย ใจดี")
  response.body   |> should.contain("user-1")
}

// ทดสอบ get_user เมื่อไม่พบ User
// Test get_user when user doesn't exist
pub fn get_user_returns_404_when_not_found_test() {
  let services = test_services(users: [])
  let context  = test_context(path_params: [#("id", "nonexistent")])
  let request  = get_request("/api/users/nonexistent")
  
  let response = user_controller.get_user(request, context, services)
  
  response.status |> should.equal(404)
  response.body   |> should.contain("error")
}

13.2 Integration Testing ด้วย Mock Services

13.2.1 Test End-to-end Flow

// test/integration/user_creation_test.gleam

// ทดสอบ flow การสร้าง user ทั้งหมด
// Test complete user creation flow
pub fn create_user_success_integration_test() {
  let services = test_services(users: [])  // เริ่มต้นไม่มี users
  let context  = test_context(user: Some("admin-id"))  // Authenticated admin
  
  let body = json.object([
    #("name",     json.string("สมหญิง สวยงาม")),
    #("email",    json.string("somying@test.com")),
    #("password", json.string("SecurePass123!")),
  ]) |> json.to_string()
  
  let request = post_request_with_body("/api/users", body)
  
  // Act
  let response = user_controller.create_user(request, context, services)
  
  // Assert — 201 Created
  response.status |> should.equal(201)
  
  // Assert — Response มี user data
  let decoded = json.decode(response.body, user.from_json)
  decoded |> should.be_ok()
  let user = result.unwrap(decoded, panic)
  user.email |> should.equal("somying@test.com")
}

13.3 HTTP-level Testing

13.3.1 Request/Response Assertions

// test/helpers/request_builder.gleam

// Helper functions สำหรับสร้าง test requests
// Helper functions for creating test requests

pub fn get_request(path: String) -> Request {
  http.Request(
    method:  http.Get,
    path:    path,
    query:   None,
    headers: [#("Accept", "application/json")],
    body:    http.Empty,
  )
}

pub fn post_request_with_body(path: String, body: String) -> Request {
  http.Request(
    method:  http.Post,
    path:    path,
    query:   None,
    headers: [
      #("Content-Type", "application/json"),
      #("Accept",       "application/json"),
    ],
    body: http.StringBody(body),
  )
}

pub fn authenticated_request(method: http.Method, path: String, token: String) -> Request {
  http.Request(
    method:,
    path:,
    query:   None,
    headers: [
      #("Authorization", "Bearer " <> token),
      #("Content-Type",  "application/json"),
    ],
    body: http.Empty,
  )
}

13.4 Test Organization และ CI

13.4.1 GitHub Actions Workflow สำหรับ Gleam Project

# .github/workflows/ci.yml

name: CI — Test and Build

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    name: Run Tests
    runs-on: ubuntu-latest
    
    services:
      # Test database
      postgres:
        image: postgres:16-alpine
        env:
          POSTGRES_USER:     test
          POSTGRES_PASSWORD: test
          POSTGRES_DB:       myapp_test
        ports: ["5432:5432"]
        options: --health-cmd pg_isready
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Erlang/OTP
        uses: erlef/setup-beam@v1
        with:
          otp-version: '26'
      
      - name: Setup Gleam
        uses: gleam-lang/setup-gleam@v1
        with:
          gleam-version: '1.4.1'
      
      - name: Download dependencies
        run: gleam deps download
      
      - name: Run tests
        run: gleam test
        env:
          DATABASE_URL: postgres://test:test@localhost:5432/myapp_test
          SECRET_KEY:   test-secret-key-for-ci
      
  build:
    name: Build Production
    runs-on: ubuntu-latest
    needs: test
    if: github.ref == 'refs/heads/main'
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup BEAM
        uses: erlef/setup-beam@v1
        with:
          otp-version: '26'
      
      - name: Setup Gleam
        uses: gleam-lang/setup-gleam@v1
        with:
          gleam-version: '1.4.1'
      
      - name: Build
        run: gleam build
      
      - name: Build Docker image
        run: docker build -t myapp:${{ github.sha }} .

14. Deployment และ Production (Deployment & Production)

14.1 Building for Production

14.1.1 Compile และ Package

# Build โปรเจกต์สำหรับ Production
# Build project for Production
gleam build

# Output จะอยู่ที่ build/
# Output will be in build/
ls build/
# dev/          ← Development build
# prod/         ← Production build (gleam build --target erlang)

# สร้าง Erlang release
# Create Erlang release
gleam export erlang-shipment

# ผลลัพธ์: โฟลเดอร์ที่มีทุกอย่างพร้อม deploy
# Result: A folder with everything ready to deploy
ls myapp-release/
# bin/
#   myapp          ← Script เริ่มต้น application
# lib/
#   myapp-1.0.0/   ← Application code
#   erlang libs    ← Dependencies

14.2 Configuration Management

14.2.1 Runtime vs Compile-time Config

// Runtime config — โหลดจาก environment ตอนเริ่ม app
// Runtime config — loaded from environment at app start

pub fn main() {
  // โหลด config ก่อนอื่น และหยุดทันทีถ้า config ผิด
  // Load config first, fail fast if config is wrong
  let config = case config.load() {
    Ok(c)  -> c
    Error(msg) -> {
      io.println_error("Configuration error: " <> msg)
      erlang.halt(1)
    }
  }
  
  // เริ่ม app ด้วย config ที่ผ่านการตรวจสอบแล้ว
  // Start app with validated config
  let services = services.create(config)
  
  server.new()
  |> server.router(router.router())
  |> server.services(services)
  |> server.port(config.port)
  |> server.listen()
}

14.3 Containerization ด้วย Docker

14.3.1 Multi-stage Dockerfile

# Dockerfile — Multi-stage build สำหรับ Gleam + BEAM
# Dockerfile — Multi-stage build for Gleam + BEAM

# ════════════════════════════════
# Stage 1: Builder
# ════════════════════════════════
FROM ghcr.io/gleam-lang/gleam:v1.4.1-erlang-alpine AS builder

# ติดตั้ง build dependencies
# Install build dependencies
RUN apk add --no-cache build-base

WORKDIR /app

# Copy dependency files ก่อน (เพื่อ cache)
# Copy dependency files first (for caching)
COPY gleam.toml manifest.toml ./
RUN gleam deps download

# Copy source และ build
# Copy source and build
COPY src/ src/
COPY priv/ priv/
RUN gleam export erlang-shipment

# ════════════════════════════════
# Stage 2: Runtime (ขนาดเล็กกว่า)
# Stage 2: Runtime (smaller image)
# ════════════════════════════════
FROM erlang:26-alpine AS runtime

WORKDIR /app

# Copy เฉพาะ runtime artifacts
# Copy only runtime artifacts
COPY --from=builder /app/build/erlang-shipment ./

# สร้าง non-root user สำหรับ security
# Create non-root user for security
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser

# Health check
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s \
  CMD wget -qO- http://localhost:8080/health || exit 1

EXPOSE 8080

# เริ่ม application
# Start application
CMD ["./bin/my_web_app", "start"]

14.3.2 Docker Compose สำหรับ Development

# docker-compose.yml

version: '3.9'

services:
  app:
    build: .
    ports: ["8080:8080"]
    environment:
      DATABASE_URL: postgres://postgres:password@db:5432/myapp
      SECRET_KEY:   dev-secret-key-not-for-production
      APP_ENV:      development
    depends_on:
      db:
        condition: service_healthy
    volumes:
      - ./src:/app/src:ro  # Hot reload สำหรับ development
  
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER:     postgres
      POSTGRES_PASSWORD: password
      POSTGRES_DB:       myapp
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./migrations:/docker-entrypoint-initdb.d:ro
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      retries: 5
  
  redis:
    image: redis:7-alpine
    ports: ["6379:6379"]

volumes:
  postgres_data:

14.4 CI/CD Pipeline

(ดู GitHub Actions workflow ในบทที่ 13.4)


14.5 Monitoring และ Observability

14.5.1 Health Check Endpoint

// src/controllers/health_controller.gleam

pub type HealthStatus {
  HealthStatus(
    status: String,
    version: String,
    uptime_seconds: Int,
    checks: List(#(String, Bool)),
  )
}

// Health check endpoint สำหรับ Load balancer
// Health check endpoint for Load balancer
pub fn health_check(_request: Request, _context: Context, services: Services) -> Response {
  let db_ok    = check_database(services.db)
  let cache_ok = check_cache(services.cache)
  
  let all_healthy = db_ok && cache_ok
  
  let status = HealthStatus(
    status:          case all_healthy { True -> "healthy" | False -> "degraded" },
    version:         "1.0.0",
    uptime_seconds:  get_uptime_seconds(),
    checks: [
      #("database", db_ok),
      #("cache",    cache_ok),
    ],
  )
  
  let status_code = case all_healthy {
    True  -> dream_http.ok
    False -> dream_http.service_unavailable
  }
  
  dream_http.json_response(
    body: health_status_to_json(status) |> json.to_string(),
    status: status_code,
  )
}

fn check_database(db: pgo.Connection) -> Bool {
  case pgo.execute("SELECT 1", db, [], dynamic.dynamic) {
    Ok(_) -> True
    Error(_) -> False
  }
}

14.5.2 BEAM Observer สำหรับ Production Debugging

# เชื่อมต่อ remote shell เข้าไปยัง running node
# Connect remote shell to running node
./bin/my_web_app remote_console

# เปิด Observer GUI (กราฟิก)
# Open Observer GUI (graphical)
:observer.start()

# ดูสถิติ processes
# View process statistics
:erlang.system_info(:process_count)

# ดู Memory usage
# View memory usage  
:erlang.memory()

Appendix A: Dream API Quick Reference

A.1 Module Index

Module Functions หลัก
dream/http Request, Response, Method, status constants
dream/router new(), route(), merge(), static()
dream/servers/mist/server new(), router(), services(), port(), listen()
dream/websocket upgrade_websocket(), broadcast(), send()
dream/middleware logger(), recovery(), cors()

Appendix B: Ecosystem Map

B.1 ตารางไลบรารีที่ใช้บ่อย

หมวด ไลบรารี คำอธิบาย hex.pm
Web dream Web toolkit gleam_dream
Web mist HTTP Server mist
HTTP gleam_http HTTP types gleam_http
JSON gleam_json JSON codec gleam_json
DB gleam_pgo PostgreSQL gleam_pgo
DB cake Query builder cake
Auth gleam_jwt JWT tokens gleam_jwt
HTML nakai HTML DSL nakai
Frontend lustre Frontend framework lustre
Testing gleeunit Test framework gleeunit
OTP gleam_otp Actors, Processes gleam_otp
Erlang gleam_erlang Erlang FFI gleam_erlang
Crypto gleam_crypto Hashing, HMAC gleam_crypto

Appendix C: ประวัติและชุมชน (Community & Further Reading)

C.1 Timeline ของ Gleam

%%{init: {'theme': 'base', 'themeVariables': {
  'primaryColor': '#458588',
  'primaryTextColor': '#ebdbb2',
  'primaryBorderColor': '#83a598',
  'lineColor': '#fabd2f',
  'secondaryColor': '#3c3836',
  'background': '#282828',
  'mainBkg': '#3c3836',
  'clusterBkg': '#32302f',
  'titleColor': '#fabd2f',
  'fontFamily': 'monospace'
}}}%%
flowchart LR
    subgraph ERA1["ยุคเริ่มต้น 2016-2019"]
        E1["2016\nLouis Pilfold\nเริ่มพัฒนา Gleam"]
        E2["2018\nFirst public release\nบน GitHub"]
        E3["2019\nGleam v0.1\nรองรับ Erlang backend"]
    end

    subgraph ERA2["ยุคเติบโต 2020-2022"]
        E4["2020\nGleam v0.9\nType aliases"]
        E5["2021\nGleam v0.15\nJavaScript backend"]
        E6["2022\nHex.pm integration\nEcosystem เริ่มโต"]
    end

    subgraph ERA3["ยุคสมบูรณ์ 2023-ปัจจุบัน"]
        E7["2023\nGleam v0.30\nuse expression"]
        E8["2024\nGleam v1.0 🎉\nStable release!"]
        E9["2024+\nDream toolkit\nEcosystem สมบูรณ์"]
    end

    E1 --> E2 --> E3 --> E4 --> E5 --> E6 --> E7 --> E8 --> E9

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


หมายเหตุ: คู่มือนี้อิงตาม Dream API ที่กำลังพัฒนา บาง API อาจมีการเปลี่ยนแปลงตาม version ล่าสุด แนะนำให้ตรวจสอบ Official Documentation ควบคู่กันเสมอ