โครง HTTP Server และ Middleware กลาง
ภาพรวม
Feature นี้เป็นกระดูกสันหลังของ service client-api ทั้งตัว ไม่ใช่ endpoint ทางธุรกิจ แต่เป็นชั้นที่ทุก endpoint ต้องวิ่งผ่าน ประกอบด้วยขั้นตอน bootstrap (โหลด config ต่อ dependency เปิด admin port และเฝ้าระวัง outage), การประกอบ gin engine, ลำดับ middleware กลาง, รูปแบบ error envelope ที่เข้ากันได้กับ NestJS เดิม และ rate limiter ระดับแอปพลิเคชัน
สิ่งที่ควรเข้าใจก่อนอ่าน feature อื่น: service นี้เป็นการ port ของ NestJS API เดิมแบบ 1:1 ทั้งรูปแบบ error, ข้อความ validation, การ serialize วันที่ (JSTime) และ route path ทั้งหมด เพื่อให้ทดแทน service Node เดิมได้โดยที่ client ไม่ต้องแก้ไขใด ๆ ทุกโดเมนเขียนด้วย pattern เดียวกันคือ entity.go → repository.go → service.go → handler.go → register.go และเขียน SQL ด้วยมือผ่าน sqlx โดยไม่ใช้ ORM
Business Flow
ขั้นตอน boot (app.New)
config.Loadอ่านค่าทั้งหมดจาก environment มาเป็น typed struct แล้วเรียกValidate()— config ที่เป็นไปไม่ได้จะทำให้ boot ล้มทันที (fail-fast) และไม่มีแพ็กเกจใดเรียกos.Getenvเองกลางโค้ด- สร้าง dependency เฉพาะตัวที่ถูกตั้งค่าไว้ (auto-enable ตาม host variable) ได้แก่ sqlx pool (
DB_HOST), named Redis client (REDIS_*) และ AMQP publisher (AMQP_URLS) แต่ละตัว implement contract 4 เมธอดName/Connect/Ping/Closeแล้วลงทะเบียนใน registry ConnectAllทำงานภายใน budget 30 วินาที และปฏิเสธชื่อ dependency ที่ซ้ำกัน เนื่องจากชื่อคือ key ที่ใช้อ้างอิงใน readiness, metric และ alert- เปิด health checker (ticker ping), admin server ที่พอร์ต
:9100(metrics/version/pprof) และ supervisor ที่เฝ้าระวัง dependency แต่ละตัวแยกกัน — เมื่อ dependency ล่มจะ retry ตามจังหวะ แจ้งเตือน chat webhook เมื่อครบ 1 นาที และเมื่อครบ 3 นาทีจะ drain แล้ว exit เพื่อให้ orchestrator รีสตาร์ต pod ใหม่ cmd/api/main.goประกาศ RabbitMQ topology (exchange, 4 queue และ DLQ) แบบ best-effort จากนั้นเรียกserver.New(deps, ...Register)เพื่อประกอบ engine
ลำดับ middleware ของทุก request (server.New)
MetricsMiddleware()— ชั้นนอกสุด วัด status และ latency ของทุก request โดย label เป็น route pattern ไม่ใช่ raw path เพื่อจำกัด cardinalitymiddleware.CORSAllowAll()— parity กับ source เดิม คือกำหนด origin, methods และ headers เป็น*แบบไม่มีเงื่อนไขmiddleware.Helmet()— security header ชุด default ยกเว้นCross-Origin-Resource-Policyเนื่องจาก source ตั้งค่าcrossOriginResourcePolicy: falsehttpx.ExceptionMiddleware— แปลง error และ panic ทุกชนิดเป็น envelope{"statusCode","message","error"}โดยฟิลด์messageเป็น string หรือ array ของ string ได้ และerrorเป็น HTTP reason phrase ส่วน error ที่ไม่ใช่*httpx.Exceptionจะกลายเป็น 500 พร้อมข้อความSystem Failure- เฉพาะสภาพแวดล้อม non-production จะเพิ่ม
gin.Logger()ไว้ชั้นหน้าสุด
middleware เพิ่มเติมบน group /api
clsx.Middleware()— สร้าง request-scoped store ประกอบด้วยtransactionId(จาก headerX-Request-Idหรือสุ่ม hex 12 ตัว),sessionId(segment ที่ 3 ของ Authorization ถ้ามี) และlang(จากx-lang) จากนั้นสะท้อนX-Request-Idกลับใน response headermiddleware.AppRateLimit— Redis-backed limiter ที่ใช้แทน ThrottlerModule เดิมซึ่งเป็น in-memory ต่อ pod โดย key ด้วย client IP ที่อ่านตามลำดับx-forwarded-for[0]→x-real-ip→x-client-ip→ socket มี sliding window (RATE_LIMIT_APP_TTL), limit (RATE_LIMIT_APP_LIMIT) และ block duration (RATE_LIMIT_BLOCK_DURATION) เมื่อเกิน limit จะตั้ง block key และตอบ 429 ทันทีในทุก request ถัดไปโดยไม่นับเพิ่ม ทั้งนี้ Redis error ทุกกรณีจะ fail open (ปล่อยผ่าน) เพื่อไม่ให้ปฏิเสธ traffic เพียงเพราะ storage มีปัญหา
Business rule ที่ควรจำ
- route ที่ไม่รู้จักจะถูก
NoRouteตอบกลับเป็น 404 envelope พร้อมข้อความรูปแบบCannot <METHOD> <path> /livez,/healthzและ/readyzmount ที่ root ของ business port ไม่ได้อยู่ใต้/apiเพราะ k8s probe และ ALB healthcheck ยิงเข้ามาที่พอร์ตนี้ ส่วน/metricsและ pprof อยู่บนพอร์ต:9100เท่านั้นและห้าม expose ออกไป/livezตรวจสอบเฉพาะตัว process โดย dependency ต้องไม่ทำให้ liveness fail เพราะการรีสตาร์ต pod ไม่ช่วยอะไรหาก database ล่ม ขณะที่/readyzผูกกับผล ping ของ dependency ทุกตัวรวมถึงสถานะ draining- ลำดับ shutdown: cancel context → หยุดรับ request → เรียก
SetDraining()ให้ readiness fail ก่อนเพื่อให้ load balancer ถอน pod ออก → ปิด admin server → ปิด dependency ย้อนลำดับ ทั้งหมดถูกจำกัดด้วยSHUTDOWN_TIMEOUT deps.DB,deps.Redisและdeps.AMQPเป็น nil ได้ทั้งหมด ทุกฟังก์ชันRegisterจึงต้อง nil-safe เพื่อให้ boot ได้แม้ไม่มี backend โดย rate limiter และ cache จะกลายเป็น no-op
ไฟล์และฟังก์ชันหลัก
| ไฟล์ | บทบาท |
|---|---|
cmd/api/main.go | binary SVC=api เรียก app.New ประกาศ AMQP topology ประกอบ engine และทำ graceful shutdown |
internal/app/app.go | app.New(ctx, cancel, service, role) — bootstrap ร่วมของ api และ worker |
internal/server/server.go | server.New(deps, registers...) ประกอบ gin engine และลำดับ middleware พร้อมนิยาม Deps และ RegisterFunc |
internal/server/metrics_middleware.go | MetricsMiddleware() |
internal/middleware/cors.go, helmet.go | CORSAllowAll(), Helmet() |
internal/middleware/ratelimit.go | AppRateLimit(rdb, ttlMs, limit, blockMs), RouteRateLimit(rdb, limit, windowSecs) |
internal/middleware/throttler.go | RateLimit(perMinute) — limiter รายนาทีที่ public-content ใช้ |
internal/httpx/exception.go | Exception และ constructor New/BadRequest/Unauthorized/Forbidden/NotFound/Conflict/InternalServerError |
internal/httpx/abort.go | Abort(c, err), ExceptionMiddleware(sentry, logger) |
internal/httpx/validate.go, checks.go | BindAndValidate, Rule, Check (mirror ของ class-validator) |
internal/httpx/jstime.go | JSTime — serialize วันที่ในรูปแบบ JS Date |
internal/clsx/clsx.go | Middleware() และ Store (transactionId/sessionId/lang) |
internal/deps/* | contract Dependency, Registry และ subpackage ต่อ backend |
internal/supervisor/supervisor.go | Start(...) และ evaluate(downFor, failures, alerted, cfg) ซึ่งเป็น pure function |
internal/config/config.go, api.go, api_load.go | typed Config ทั้งหมด |
Route ที่ประกอบไว้ในชั้นนี้ ได้แก่ GET /livez, GET /healthz และ GET /readyz ที่ root ส่วน /api และ /api/health ดูรายละเอียดได้ที่ Health Check และ Probe
จุดเชื่อมต่อกับ Service อื่น
- PostgreSQL ผ่าน sqlx pool (
internal/platform/db) โดยทุก repository ใช้ pool เดียวกัน - Redis ผ่าน named manager (
internal/platform/redisx) โดย client ชื่อredisถูกใช้ทั้งงาน rate limit, cache และ OTP session - RabbitMQ ผ่าน
internal/amqp(HA publisher, confirms,EnsureQueue) รวม 4 queue ได้แก่line_change_richmenu,friend_track_event_trigger,booking_notificationและbooking_event_trigger - Object storage (S3/MinIO) ผ่าน
internal/storagexและinternal/s3x - LINE Platform ผ่าน
internal/linehttpดูรายละเอียดที่ การตรวจสิทธิ์ LIFF Token - ทุก feature ในหมวดนี้ถูก mount ผ่าน
RegisterFuncที่ระบุไว้ในcmd/api/main.go - ฝั่ง client-web ที่เกี่ยวข้องโดยตรงคือ feature
app-shellซึ่งดูแล axios instance, base URL และการอ่าน error code จาก envelope