คู่มือฉบับสมบูรณ์ สำหรับนักพัฒนาที่ต้องการสร้าง Web Application แบบ Type-safe, Fault-tolerant บน BEAM Virtual Machine ด้วยภาษา Gleam และ Web Toolkit ชื่อ Dream
Dream คือ Web Toolkit สำหรับภาษา Gleam ที่ออกแบบมาบนหลักการสำคัญ: ความชัดเจนเหนือความสะดวก (Explicit over Implicit)
ในขณะที่ Framework ทั่วไปอย่าง Rails, Django หรือ Laravel ใช้แนวคิด "Convention over Configuration" ซึ่งซ่อนรายละเอียดไว้เบื้องหลัง Dream เลือกเปิดเผยทุกอย่างออกมาอย่างโปร่งใส
| คุณสมบัติ | 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 | ❌ ไม่มีมาตรฐาน |
BEAM (Bogdan/Björn's Erlang Abstract Machine) คือ Virtual Machine ที่ถูกสร้างมาเพื่อรองรับระบบ Distributed, Fault-tolerant ตั้งแต่ต้น ไม่ใช่สิ่งที่เพิ่มเข้ามาทีหลัง
%%{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
"Let it crash" คือปรัชญาของ Erlang/OTP — เมื่อ Process ล้มเหลว Supervisor จะ Restart ให้อัตโนมัติ ระบบโดยรวมยังคงทำงานต่อได้
ตัวอย่างบริษัทที่ใช้ BEAM ในระบบ Production จริง:
%%{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
%%{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
| ไลบรารี | บทบาท | 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 |
# ติดตั้ง 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
# สร้างโปรเจกต์ 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
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/
# 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"
# ดึง dependencies ทั้งหมด
# Fetch all dependencies
gleam deps download
# รันโปรเจกต์
# Run the project
gleam run
# รัน tests
# Run tests
gleam test
fn controller(request, context, services)Dream แยก argument ออกเป็นสามส่วนเพื่อความชัดเจน:
request — HTTP Request ที่เข้ามา (Immutable, per-request)context — ข้อมูลที่ Middleware ใส่เพิ่มเข้ามา เช่น ข้อมูล User ที่ผ่าน Auth แล้ว (per-request, mutable during middleware)services — ทรัพยากรที่แชร์กันทั้งแอป เช่น database connection, cache (shared across requests)// 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,
)
}
}
// 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: [],
)
}
// 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
}
// ประเภทสำคัญใน 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
)
}
// 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
// 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...
}
// 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,
)
}
}
// 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])
}
| 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) |
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
// 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
}
}
// 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,
)
}
}
}
}
// 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)
}
})
}
// การใช้ |> เพื่อประกอบ 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])
Services Pattern คือการรวม dependencies ทั้งหมดของแอปพลิเคชันไว้ใน record เดียว แทนที่จะใช้ Global Variables หรือ Singleton
%%{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
// 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:,
)
}
| 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 |
// 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, "สมชาย")
}
// 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)
}
// 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,
)
}
}
// 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)
}
}
}
// 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)
}
// 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\""),
],
)
}
// 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,
)
}
// 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)
}
// 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"),
],
)
}
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)
}
}
// 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()))
}
}
// 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
}
}
// 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)
}
}
// 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 วินาที
)
)
}
// 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)
}
%%{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
-- 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);
// 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)
}
// 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
}
// 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("+")]),
])
}
// 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>
// ตัวอย่างการใช้ 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)) })
}
// 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()
}
// สร้าง 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,
)
}
}
}
}
}
// 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)),
])
}
// 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)),
)
}
}
}
}
}
// 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)
}
}
}
}
}
Token Bucket ทำงานบนสูตร:
โดยที่:
ตัวอย่างการคำนวณ:
สมมติว่า capacity = 10 requests, refill_rate = 2 token/วินาที, ปัจจุบัน tokens = 3, Δt = 2.5 วินาที
// 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))
}
}
// 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=()"),
]),
)
}
}
// 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
// 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")
}
// 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")
}
// 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,
)
}
# .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 }} .
# 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
// 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()
}
# 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"]
# 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:
(ดู GitHub Actions workflow ในบทที่ 13.4)
// 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
}
}
# เชื่อมต่อ 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()
| 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() |
| หมวด | ไลบรารี | คำอธิบาย | 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 |
%%{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
หมายเหตุ: คู่มือนี้อิงตาม Dream API ที่กำลังพัฒนา บาง API อาจมีการเปลี่ยนแปลงตาม version ล่าสุด แนะนำให้ตรวจสอบ Official Documentation ควบคู่กันเสมอ