Imported from ngocthanhnt00/livestream-demo (
.agents/skills/base-structure-source-backend/SKILL.md). Install upstream withnpx skills add ngocthanhnt00/livestream-demo --skill base-structure-source-backend. Copyright stays with the author.
Quy trình Cấu trúc API & Thiết kế Nâng cao dự án Delippy
Tài liệu này hướng dẫn cách xây dựng các API endpoints mới, áp dụng Rate Limiting, xử lý ngoại lệ tập trung (Global Exception Handling) để giữ cho Controller luôn gọn gàng (Thin Controller) và giới thiệu các mẫu thiết kế (Design Patterns) nâng cao đang được áp dụng hiệu quả trong dự án Delippy.
1. Luồng Cấu trúc API (API Flow Structure)
Mọi API mới trong dự án đều phải tuân thủ nghiêm ngặt mô hình luồng dữ liệu 4 lớp (Multi-tier Architecture):
$$\text{Request} \longrightarrow \text{Controller} \longrightarrow \text{Service} \longrightarrow \text{Resource/Response}$$
Sơ đồ hoạt động và nhiệm vụ từng phần:
- FormRequest:
- Đảm nhận toàn bộ vai trò Xác thực (Validation) và Phân quyền (Authorization) của request đầu vào.
- Định nghĩa phương thức
toData()hoặcvalidated()để đóng gói payload đầu vào dạng array/DTO trước khi chuyển đi.
- Controller:
- Nhận FormRequest đã được validate tự động bởi Laravel.
- Đóng vai trò làm Router trung gian: trích xuất các tham số cần thiết, chuyển tiếp đến Service tương ứng.
- Trả về phản hồi JSON thống nhất qua các phương thức của ApiController (như
respondWithData(),respondCreatedCustomData(),respondBadRequest(), v.v.).
- Service:
- Nơi xử lý logic nghiệp vụ chính (Business Logic), các ràng buộc của Domain, gọi tích hợp bên thứ ba (3rd party), thao tác trực tiếp với Database.
- Đảm bảo tính nguyên tử (Atomicity) cho các luồng ghi/cập nhật dữ liệu bằng cách sử dụng
DB::transaction(...). - Ném (throw) ra các Custom Exception cụ thể khi xảy ra lỗi logic nghiệp vụ. Không truy cập trực tiếp vào đối tượng HTTP Request.
- Resource (Eloquent Resource):
- Định dạng lại dữ liệu trả về cho Client.
- Tránh việc trả về trực tiếp Model thô để bảo mật các cột nhạy cảm và giữ cấu trúc API luôn ổn định.
Minh họa code mẫu chuẩn chỉnh:
Bước A: Định nghĩa Request (app/Http/Requests/...)
namespace App\Http\Requests\Order;
use Illuminate\Foundation\Http\FormRequest;
class StoreOrderRequest extends FormRequest
{
public function authorize(): bool
{
return true; // Phân quyền nếu cần thiết
}
public function rules(): array
{
return [
'payment_method' => ['required', 'string', 'in:cod,sepay'],
'shipping_type' => ['required', 'string'],
'customer_name' => ['required', 'string', 'max:191'],
'customer_phone' => ['required', 'string', 'regex:/^[0-9]{10}$/'],
'coupon_code' => ['nullable', 'string'],
];
}
/**
* Chuẩn hóa payload trước khi truyền vào Service
*/
public function toData(): array
{
return $this->validated();
}
}
Bước B: Định nghĩa Controller (app/Http/Controllers/Delippy/v1/...)
Quy tắc quan trọng: Controller kế thừa ApiController, KHÔNG chứa logic nghiệp vụ và KHÔNG có try-catch để giữ Thin Controller.
namespace App\Http\Controllers\Delippy\v1\Order;
use App\Http\Controllers\Api\v1\ApiController;
use App\Http\Requests\Order\StoreOrderRequest;
use App\Http\Resources\OrderResource;
use App\Services\Delippy\OrderService;
use Illuminate\Http\JsonResponse;
class OrderController extends ApiController
{
// Constructor injection Service
public function __construct(private readonly OrderService $orderService)
{
}
public function store(StoreOrderRequest $request): JsonResponse
{
// Gọi Service xử lý happy path
$order = $this->orderService->placeOrder(
auth('api-mobile')->id(),
$request->toData()
);
// Trả về Resource đã được format thống nhất
return $this->setStatusCode(201)
->setReturnCode(self::RESPONSE_CREATED)
->respondWithData(new OrderResource($order), __('order.placed_success'));
}
}
Bước C: Định nghĩa Service (app/Services/Delippy/...)
namespace App\Services\Delippy;
use App\Exceptions\EmptyCartException;
use App\Exceptions\InvalidCouponException;
use App\Models\Order;
use Illuminate\Support\Facades\DB;
class OrderService
{
public function placeOrder(int $userId, array $data): Order
{
// 1. Kiểm tra nghiệp vụ và ném Custom Exception nếu có lỗi
$cart = $this->getCart($userId);
if (empty($cart)) {
throw new EmptyCartException(__('cart.empty'));
}
if (isset($data['coupon_code']) && !$this->isValidCoupon($data['coupon_code'])) {
throw new InvalidCouponException(__('coupon.invalid'));
}
// 2. Thực thi Transaction đảm bảo nguyên tố dữ liệu
return DB::transaction(function () use ($userId, $data) {
$order = Order::create([
'user_id' => $userId,
'status' => 'pending',
// các trường dữ liệu khác...
]);
// Xử lý thêm các bảng phụ (Order Items, Payments...)
return $order;
});
}
}
Bước D: Định nghĩa Resource (app/Http/Resources/...)
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class OrderResource extends JsonResource
{
public function toArray($request): array
{
return [
'id' => $this->id,
'order_number' => $this->order_number,
'status' => $this->status,
'payment_status' => $this->payment_status,
'payment_method' => $this->payment_method,
'created_at' => $this->created_at?->toIso8601String(),
];
}
}
2. Quản lý Tần suất Gọi API (Rate Limiting)
Dự án Delippy sử dụng cấu hình giới hạn tần suất gọi API thông qua RateLimiter của Laravel trong AppServiceProvider.
Các Rate Limiter đã định nghĩa sẵn:
| Tên Limiter | Quy tắc giới hạn | Phù hợp cho Endpoint |
|---|---|---|
api |
Thành viên: 60 requests/phútKhách (IP): 30 requests/phút | Các API thông thường |
api-login |
5 requests/phút theo IP + Credential20 requests/giờ theo Credential | API đăng nhập, xác thực |
api-search |
20 requests/phút theo IP | API tìm kiếm sản phẩm, bài viết |
api-order |
5 requests/phút theo User ID (hoặc IP nếu chưa login) | API đặt hàng |
api-review |
5 requests mỗi 10 phút | API đánh giá đơn hàng / vendor |
api-otp |
3 req/phút theo IP2 req/phút theo email10 req/giờ theo email | API gửi / yêu cầu mã OTP |
Cách áp dụng Rate Limiter trong Routing (routes/api.php hoặc files route con):
Sử dụng middleware throttle:tên_limiter trên các nhóm route hoặc các route riêng lẻ:
// Áp dụng cho một route cụ thể (ví dụ: đặt hàng)
Route::post('/orders', [OrderController::class, 'store'])
->middleware('throttle:api-order');
// Áp dụng cho nhóm API tìm kiếm
Route::get('/products/search', [ProductController::class, 'search'])
->middleware('throttle:api-search');
3. Cấu trúc Xử lý Ngoại lệ (Global Exception Handling) cho Thin Controller
Để giữ Controller luôn ngắn gọn và sạch sẽ (Thin Controller), tránh viết khối try-catch bọc quanh toàn bộ logic ở Controller. Thay vào đó, toàn bộ Exception của hệ thống phải được bắt và định dạng response tự động ở tầng Global.
Có hai cách tiếp cận được khuyên dùng trong dự án Delippy:
Cách A: Tự định dạng Response trong Exception (Self-Rendering Exception)
Nếu một exception có phương thức render(), Laravel sẽ tự động gọi phương thức này khi exception xảy ra mà không cần khai báo ở Handler.php.
namespace App\Exceptions;
use Exception;
use Illuminate\Http\JsonResponse;
use App\Http\Controllers\Api\v1\ApiController;
class EmptyCartException extends Exception
{
public function render($request): JsonResponse
{
return response()->json([
'success' => false,
'code' => ApiController::RESPONSE_BAD_REQUEST,
'message' => $this->getMessage() ?: __('cart.empty'),
'data' => null,
'trace_id' => $request->header('X-Request-ID'),
], 400);
}
}
Cách B: Đăng ký Render trong Exception Handler (app/Exceptions/Handler.php)
Đây là phương pháp khuyên dùng để gom tất cả các Exception về một mối, giúp quản lý định dạng phản hồi nhất quán dễ dàng hơn.
Thêm các khai báo renderable trong phương thức register() của Handler.php:
namespace App\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use App\Http\Controllers\Api\v1\ApiController;
use Illuminate\Validation\ValidationException;
use Illuminate\Http\Exceptions\ThrottleRequestsException;
use App\Helpers\LogHelper;
use Throwable;
class Handler extends ExceptionHandler
{
protected $dontFlash = [
'current_password',
'password',
'password_confirmation',
];
public function register(): void
{
// 1. Ghi log các lỗi không mong muốn (hệ thống)
$this->reportable(function (Throwable $e) {
if ($this->shouldReport($e)) {
LogHelper::writeLog(LogHelper::ERROR, 'system', $e, request());
}
});
// 2. Bắt các Custom Business Exception
$this->renderable(function (EmptyCartException $e, $request) {
return $this->buildApiResponse($e->getMessage(), ApiController::RESPONSE_BAD_REQUEST, 400);
});
$this->renderable(function (InvalidCouponException $e, $request) {
return $this->buildApiResponse($e->getMessage(), ApiController::RESPONSE_BAD_REQUEST, 400);
});
// 3. Bắt lỗi vượt quá Rate Limit (Http 429) để trả về cấu trúc thống nhất
$this->renderable(function (ThrottleRequestsException $e, $request) {
return $this->buildApiResponse(
__('auth.too_many_attempts') ?: 'Too many requests. Please slow down.',
ApiController::RESPONSE_TOO_MANY_REQUESTS,
429
);
});
}
/**
* Xây dựng JSON response thống nhất cho API lỗi
*/
private function buildApiResponse(string $message, int $returnCode, int $statusCode)
{
return response()->json([
'success' => false,
'code' => $returnCode,
'message' => $message,
'data' => null,
'trace_id' => request()->header('X-Request-ID'),
], $statusCode);
}
}
4. Các Mẫu Thiết Kế Nâng Cao trong Dự Án (Advanced Design Patterns)
Dự án Delippy tích hợp một số mẫu thiết kế rất tốt giúp tối ưu hiệu năng, bảo mật và khả năng bảo trì:
A. Thiết kế Domain Logic Phi Trạng Thái (Stateless Domain Calculators)
- Ví dụ tiêu biểu: OrderCalculator.php
- Nội dung: Các logic tính toán số tiền, thuế, chiết khấu, điểm mua sắm (Shopping Points) được tách hoàn toàn ra khỏi Model hay cơ sở dữ liệu. Nó nhận vào dữ liệu thô (hoặc Collections) và trả về một đối tượng chứa kết quả dưới dạng DTO bất biến (
readonly classPHP 8.2 như SePayWebhookData.php). - Lợi ích: Giúp code nghiệp vụ tài chính dễ dàng viết Unit Test, tính toán chuẩn xác và độc lập khỏi các thay đổi của database.
B. Sử dụng Backed Enums Đầy Đủ Logic (Rich Domain Enums)
- Ví dụ tiêu biểu: OrderStatus.php
- Nội dung: Enums không chỉ đơn thuần khai báo các trạng thái tĩnh dạng chuỗi, mà còn khai báo thêm các logic điều khiển đi kèm. Ví dụ:
public function isCancellable(): bool { return in_array($this, [self::Pending, self::Processing]); } public function progressRank(): int { return match($this) { self::Pending => 0, self::Processing => 1, self::OnDelivery => 2, self::Completed => 3, self::Declined => -1, }; } - Ứng dụng: Thuật toán đồng bộ trạng thái đơn hàng lớn từ các shop con ở VendorOrderService.php sử dụng hàm
progressRank()để xếp hạng trạng thái. Trạng thái của đơn hàng tổng được quyết định bởi shop có tiến trình chậm nhất (ví dụ: một shop đã giao hàng nhưng shop còn lại đang xử lý thì đơn hàng tổng vẫn ở trạng thái "Đang xử lý").
C. Composite Pattern Cho Hệ Thống Real-time Notifications
- Ví dụ tiêu biểu: CompositePaymentNotifier.php
- Nội dung: Đóng gói nhiều kênh thông báo (
FcmPaymentNotifiervàFirestoreNotifier) dưới một interface chungPaymentRealtimeNotifier. - Lợi ích: Giúp phân phát (fan out) sự kiện cập nhật thanh toán tới nhiều dịch vụ khác nhau song song mà không sợ lỗi ở một kênh (ví dụ: FCM lỗi kết nối) làm gián đoạn kênh còn lại. Dễ dàng cắm thêm kênh mới (SMS, Mail) chỉ bằng việc đăng ký thêm trong
AppServiceProvider.
D. An Toàn Webhook Thanh Toán (Webhook Security & Idempotency)
- Ví dụ tiêu biểu: SePayWebhookService.php
- Nội dung:
- Idempotency (Bảo vệ trùng lặp): Luôn kiểm tra giao dịch đã xử lý trước đó thông qua
OrderPayment::where('gateway_transaction_id', $data->id)->exists()để phòng ngừa Webhook từ cổng thanh toán gửi trùng lặp. - Late Payments (Thanh toán muộn): Khi tiền về muộn sau khi mã QR thanh toán hết hạn mềm, hệ thống không bỏ qua giao dịch mà tự động ghi nhận và phát cảnh báo CSKH để xử lý thủ công, bảo vệ tối đa lợi ích khách hàng.
- Amount Verification: Luôn đối chiếu số tiền thực tế nhận từ Webhook với số tiền đơn hàng ghi nhận trong Database để phòng tránh gian lận thay đổi thông tin.
- Idempotency (Bảo vệ trùng lặp): Luôn kiểm tra giao dịch đã xử lý trước đó thông qua
5. Checklist Thực thi Dự án (Developer Verification Checklist)
Mỗi khi phát triển hay chỉnh sửa API, nhà phát triển (hoặc AI agent) cần xác nhận các mục sau:
- Luồng dữ liệu đi đúng hướng: Request -> Controller -> Service -> Resource.
- Controller hoàn toàn không chứa logic xác thực hay truy vấn DB trực tiếp.
- Controller KHÔNG sử dụng khối
try-catchcục bộ để bắt exception nghiệp vụ; thay vào đó sử dụng Global Exception Handler. - Các tính toán nghiệp vụ phức tạp được tách thành các Stateless Calculator và truyền nhận bằng DTO.
- Sử dụng Backed Enums cùng các hàm phụ trợ (
progressRank,isCancellable) thay vì hardcode chuỗi hoặc số nguyên trạng thái. - Mọi thay đổi nghiệp vụ quan trọng đều chạy trong
DB::transaction(...)ở tầng Service. - Đã đăng ký/áp dụng đúng Middleware
throttle:tên-limitertrên Router cho các API nhạy cảm (đăng nhập, tìm kiếm, đặt hàng, gửi OTP). - Việc ghi log lỗi nghiêm trọng bắt buộc thông qua LogHelper và channel đã được định nghĩa tại
config/logging.php. - Logic Webhook thanh toán có kiểm tra trùng lặp (Idempotency) và kiểm soát lỗi thanh toán muộn (Late Payment).1