REPR (Request-Endpoint-Response)
นิยาม API endpoint ด้วยสามส่วน: Request, Endpoint, Response
เมื่อสร้าง web API ด้วย ASP.NET Core MVC แบบดั้งเดิม เรามักเริ่มจาก Controller 1 class ต่อ resource แล้วยัด action หลายตัว (GET, POST, PUT, DELETE) ไว้ใน class เดียวกัน เมื่อ project โต controller เหล่านี้จะกลายเป็น “god class” ที่มี dependency พ่วงเยอะ มี action หลายสิบตัวที่ไม่เกี่ยวข้องกัน และ share constructor/field กันโดยไม่จำเป็น — ทั้งที่แต่ละ action มักไม่ได้ใช้ dependency เดียวกันเลย
ปัญหาอีกชั้นคือ MVC ถูกออกแบบมาสำหรับ app UI ที่ render View เป็น HTML ตั้งแต่แรก ไม่ใช่สำหรับ API ที่คืนค่าเป็น JSON การเอา MVC มาทำ API จึงมักต้องผูก ViewModel, model binding, และ action filter ที่ไม่ตรงกับความต้องการจริงของ endpoint
REPR (Request-Endpoint-Response) ซึ่ง Steve Smith (ผู้เขียน DevIQ) เป็นผู้บัญญัติชื่อ แก้ปัญหานี้ด้วยแนวคิดง่าย ๆ: มอง API แต่ละ endpoint เป็น “หน่วยงานเดียว” ที่ประกอบด้วยสามส่วน
- Request — รูปร่างของข้อมูล input ที่ endpoint ต้องการ
- Endpoint — ตรรกะที่ endpoint ทำงานเมื่อได้รับ request (มี method เดียว)
- Response — รูปร่างของข้อมูล output ที่ endpoint คืนกลับ
แต่ละ endpoint กลายเป็น class ของตัวเอง ไม่ share กับ endpoint อื่น สอดคล้องโดยตรงกับ Single Responsibility Principle — 1 class หนึ่งเหตุผลในการเปลี่ยนแปลง และยังจับคู่เป็นธรรมชาติกับแนวคิด Vertical Slice ที่จัด code ตาม feature แทนที่จะจัดตาม layer
โครงสร้าง
หัวข้อที่มีชื่อว่า “โครงสร้าง”REPR ไม่ต้องมี controller กลางที่รวม action หลายตัว แต่ละ endpoint เป็น class อิสระที่ผูก Request type กับ Response type ของตัวเองผ่าน generic base class (เช่นใน library FastEndpoints)
classDiagram
class GetOrderRequest {
+Guid OrderId
}
class OrderResponse {
+Guid Id
+string Status
+decimal Total
}
class EndpointBase {
+Configure()
+HandleAsync(req, ct)
}
class GetOrderEndpoint {
-IOrderRepository repo
+Configure()
+HandleAsync(req, ct)
}
EndpointBase <|-- GetOrderEndpoint
GetOrderEndpoint ..> GetOrderRequest
GetOrderEndpoint ..> OrderResponse
จุดสำคัญของโครงสร้างนี้คือ ไม่มี controller กลาง ที่ endpoint หลายตัวต้อง share กัน — GetOrderEndpoint ผูกกับ GetOrderRequest/OrderResponse ของมันเองเท่านั้น หาก endpoint อื่นต้องการ dependency ต่างกัน ก็ประกาศ constructor ของตัวเองได้อย่างอิสระ ไม่กระทบ endpoint อื่นเลย
การทำงาน
หัวข้อที่มีชื่อว่า “การทำงาน”เมื่อ HTTP request เข้ามา framework (ไม่ว่าจะเป็น ASP.NET Core MVC ที่ครอบด้วย convention, Minimal API, หรือ library เฉพาะทางอย่าง FastEndpoints) จะ:
- Bind ข้อมูลจาก HTTP body/query/route เข้าเป็น Request object
- (มักมี) รัน validation บน Request object ก่อนเข้าสู่ business logic
- ส่ง Request เข้าสู่ Endpoint ซึ่งมี method เดียว (เช่น
HandleAsync) ที่ทำ business logic ทั้งหมดของ endpoint นั้น — เรียก repository, domain service, หรือส่งต่อ Command/Query ผ่าน Mediator ก็ได้ - Endpoint สร้าง Response object แล้ว serialize กลับเป็น HTTP response
sequenceDiagram
participant Client
participant Endpoint as GetOrderEndpoint
participant Repo as IOrderRepository
Client->>Endpoint: HTTP GET /orders/123
Endpoint->>Endpoint: bind + validate GetOrderRequest
Endpoint->>Repo: GetByIdAsync(orderId)
Repo-->>Endpoint: Order
Endpoint->>Endpoint: map เป็น OrderResponse
Endpoint-->>Client: HTTP 200 + OrderResponse
สังเกตว่าไม่มีขั้นตอนไหนที่ endpoint นี้ต้องรู้จักหรือ share state กับ endpoint อื่น — ต่างจาก MVC controller ที่ action หลายตัวอยู่ใน class เดียวและมัก share constructor dependency ร่วมกันโดยไม่จำเป็น
ตัวอย่าง code
หัวข้อที่มีชื่อว่า “ตัวอย่าง code”ตัวอย่างด้านล่างจำลองการใช้ REPR แบบที่ library อย่าง FastEndpoints ใช้จริง โดยแยก Request, Response และ Endpoint ออกเป็นคนละ class และมี validation ในตัว
// 1) Request — รูปร่างของ inputpublic class GetOrderRequest{ public Guid OrderId { get; set; }}
// 2) Response — รูปร่างของ outputpublic class OrderResponse{ public Guid Id { get; set; } public string Status { get; set; } = default!; public decimal Total { get; set; }}
// 3) Endpoint — ตรรกะทั้งหมดของ endpoint นี้อยู่ใน class เดียวpublic class GetOrderEndpoint : Endpoint<GetOrderRequest, OrderResponse>{ private readonly IOrderRepository _repository;
// dependency เฉพาะของ endpoint นี้เท่านั้น ไม่ share กับ endpoint อื่น public GetOrderEndpoint(IOrderRepository repository) { _repository = repository; }
public override void Configure() { Get("/orders/{OrderId}"); // กำหนด route + HTTP verb ของ endpoint นี้ AllowAnonymous(); }
public override async Task HandleAsync(GetOrderRequest req, CancellationToken ct) { var order = await _repository.GetByIdAsync(req.OrderId, ct); if (order is null) { await SendNotFoundAsync(ct); return; }
// map domain model เป็น response model โดยตรง ไม่ผ่าน ViewModel กลาง await SendAsync(new OrderResponse { Id = order.Id, Status = order.Status.ToString(), Total = order.Total }, cancellation: ct); }}
// endpoint อีกตัวสำหรับ use case อื่น — คนละ class คนละ dependency โดยสิ้นเชิงpublic class CreateOrderEndpoint : Endpoint<CreateOrderRequest, CreateOrderResponse>{ private readonly IOrderService _orderService;
public CreateOrderEndpoint(IOrderService orderService) => _orderService = orderService;
public override void Configure() => Post("/orders");
public override async Task HandleAsync(CreateOrderRequest req, CancellationToken ct) { var orderId = await _orderService.CreateAsync(req.CustomerId, req.Items, ct); await SendCreatedAtAsync<GetOrderEndpoint>( new { OrderId = orderId }, new CreateOrderResponse { Id = orderId }, cancellation: ct); }}จะเห็นว่าแต่ละ endpoint test ได้อย่างอิสระ — สร้าง GetOrderEndpoint ขึ้นมาพร้อม mock IOrderRepository แล้วเรียก HandleAsync โดยตรง ไม่ต้อง bootstrap MVC pipeline ทั้งชุด
เมื่อไหร่ควรใช้
หัวข้อที่มีชื่อว่า “เมื่อไหร่ควรใช้”- กำลังสร้าง web API (ไม่ใช่ app ที่ render HTML view) และต้องการโครงสร้างที่โฟกัสที่ endpoint โดยตรง
- ทีมต้องการเลิกใช้ controller อ้วนที่มี action จำนวนมาก share constructor เดียวกัน
- ต้องการจัด code ตาม Vertical Slice — แต่ละ feature อยู่ครบในที่เดียว (request, validation, handler, response)
- ต้องการ endpoint ที่ test ง่าย เพราะแต่ละ class มี dependency น้อยและชัดเจนเฉพาะตัว
- ใช้ร่วมกับ CQRS หรือ Mediator ได้ดี — Endpoint ทำหน้าที่เป็นชั้นบาง ๆ ที่ส่งต่อไปยัง Command/Query handler
เมื่อไหร่ไม่ควรใช้
หัวข้อที่มีชื่อว่า “เมื่อไหร่ไม่ควรใช้”- app ที่ยังต้อง render View (HTML) เป็นหลัก — MVC หรือ Razor Pages ยังเหมาะกว่า
- API ขนาดเล็กมากที่มี endpoint ไม่กี่ตัว ซึ่งความซับซ้อนของ controller ยังไม่ใช่ปัญหา การเพิ่ม file/class อาจไม่คุ้ม
- ทีมที่ยังไม่คุ้นกับแนวคิดนี้และไม่มีการเซ็ต architecture test คุมวินัย เพราะไม่มีอะไรบังคับไม่ให้ endpoint เติบโตจนมี logic เกินความรับผิดชอบเดิม
- project ที่ผูกกับ tooling หรือ convention ของ MVC controller อยู่แล้วอย่างลึก (เช่น filter, model binding แบบ custom จำนวนมาก) การย้ายอาจไม่คุ้มค่า
ข้อดีและข้อเสีย
หัวข้อที่มีชื่อว่า “ข้อดีและข้อเสีย”| ด้าน | ข้อดี | ข้อเสีย |
|---|---|---|
| โครงสร้าง code | แต่ละ endpoint แยกอิสระ อ่านง่าย ตาม SRP | จำนวน file/class เพิ่มขึ้นมาก เมื่อ API มี endpoint จำนวนมาก |
| Dependency | Constructor ของแต่ละ endpoint มีแค่สิ่งที่ endpoint นั้นใช้จริง | ต้อง DI/register หลาย class มากขึ้น |
| Testability | test แยก endpoint ได้ง่าย ไม่ต้องพ่วง MVC pipeline | — |
| Performance | endpoint ที่ implement ผ่าน library อย่าง FastEndpoints มักเร็วกว่า MVC controller เพราะ bypass filter pipeline ที่ไม่ใช้ | — |
| ความเป็นผู้ใหญ่ของ pattern | แก้ปัญหา controller อ้วนได้ตรงจุด | ยังค่อนข้างใหม่ เอกสาร/ตัวอย่างในชุมชนน้อยกว่า MVC; ต้องมีวินัยทีม (เช่น architecture test) ไม่ให้ endpoint โตเกินขอบเขต |
| Swagger/OpenAPI | — | การจัดกลุ่ม endpoint ใน Swagger UI ต้องตั้งค่า tag เพิ่มเอง เพราะไม่มี controller ให้ group ตามธรรมชาติ |