技術 7 分鐘閱讀

Laravel API 設計最佳實踐:打造乾淨好維護的 API

Laravel API 設計最佳實踐:打造乾淨好維護的 API

前言

API 設計的好壞直接影響前後端協作效率和系統的可維護性。這篇文章整理了我在 Laravel 專案中常用的 API 設計模式和最佳實踐。

RESTful 路由設計

使用資源路由

Laravel 的資源路由已經幫你設計好標準的 CRUD 路由:

// routes/api.php
Route::apiResource('products', ProductController::class);

這會自動產生以下路由:

方法URI動作
GET/api/productsindex
POST/api/productsstore
GET/api/products/{id}show
PUT/api/products/{id}update
DELETE/api/products/{id}destroy

巢狀資源

Route::apiResource('products.reviews', ReviewController::class)
    ->scoped(['review' => 'id']);

產生的路由如 GET /api/products/{product}/reviews

API 版本控制

Route::prefix('v1')->group(function () {
    Route::apiResource('products', V1\ProductController::class);
});

Route::prefix('v2')->group(function () {
    Route::apiResource('products', V2\ProductController::class);
});

統一回應格式

建立 API Response Trait

trait ApiResponse
{
    protected function success($data, string $message = 'Success', int $code = 200)
    {
        return response()->json([
            'success' => true,
            'message' => $message,
            'data' => $data,
        ], $code);
    }

    protected function error(string $message, int $code = 400, $errors = null)
    {
        $response = [
            'success' => false,
            'message' => $message,
        ];

        if ($errors) {
            $response['errors'] = $errors;
        }

        return response()->json($response, $code);
    }
}

在 Controller 中使用

class ProductController extends Controller
{
    use ApiResponse;

    public function index(Request $request)
    {
        $products = Product::with('category')
            ->paginate($request->get('per_page', 15));

        return $this->success($products);
    }

    public function store(StoreProductRequest $request)
    {
        $product = Product::create($request->validated());

        return $this->success($product, '商品建立成功', 201);
    }
}

API Resource 轉換資料

不要直接回傳 Model,用 Resource 來控制輸出格式:

// app/Http/Resources/ProductResource.php
class ProductResource extends JsonResource
{
    public function toArray($request): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'slug' => $this->slug,
            'price' => $this->price,
            'formatted_price' => 'NT$ ' . number_format($this->price),
            'category' => new CategoryResource($this->whenLoaded('category')),
            'images' => ImageResource::collection($this->whenLoaded('images')),
            'is_available' => $this->stock > 0,
            'created_at' => $this->created_at->toISOString(),
        ];
    }
}

在 Controller 中使用:

public function show(Product $product)
{
    $product->load(['category', 'images', 'reviews']);

    return new ProductResource($product);
}

public function index()
{
    $products = Product::with('category')->paginate(15);

    return ProductResource::collection($products);
}

表單驗證

使用 Form Request

// app/Http/Requests/StoreProductRequest.php
class StoreProductRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'name' => ['required', 'string', 'max:255'],
            'price' => ['required', 'numeric', 'min:0'],
            'category_id' => ['required', 'exists:categories,id'],
            'description' => ['nullable', 'string'],
            'images' => ['nullable', 'array'],
            'images.*' => ['image', 'max:2048'],
        ];
    }

    public function messages(): array
    {
        return [
            'name.required' => '商品名稱為必填',
            'price.required' => '價格為必填',
            'price.min' => '價格不能小於 0',
        ];
    }
}

自動回傳驗證錯誤

Laravel 在 API 請求時會自動回傳 422 狀態碼和錯誤訊息,不需要手動處理。

錯誤處理

全域例外處理

app/Exceptions/Handler.php 中統一處理 API 錯誤:

public function render($request, Throwable $e)
{
    if ($request->expectsJson()) {
        if ($e instanceof ModelNotFoundException) {
            return response()->json([
                'success' => false,
                'message' => '找不到指定的資源',
            ], 404);
        }

        if ($e instanceof AuthenticationException) {
            return response()->json([
                'success' => false,
                'message' => '請先登入',
            ], 401);
        }

        if ($e instanceof AuthorizationException) {
            return response()->json([
                'success' => false,
                'message' => '沒有權限執行此操作',
            ], 403);
        }
    }

    return parent::render($request, $e);
}

篩選與排序

使用 Query Builder 的篩選模式

public function index(Request $request)
{
    $query = Product::query();

    // 篩選
    if ($request->has('category_id')) {
        $query->where('category_id', $request->category_id);
    }

    if ($request->has('min_price')) {
        $query->where('price', '>=', $request->min_price);
    }

    if ($request->has('search')) {
        $query->where('name', 'like', "%{$request->search}%");
    }

    // 排序
    $sortField = $request->get('sort', 'created_at');
    $sortDirection = $request->get('direction', 'desc');
    $query->orderBy($sortField, $sortDirection);

    return ProductResource::collection(
        $query->with('category')->paginate($request->get('per_page', 15))
    );
}

API 文件

使用 Scribe 自動產生文件

composer require knuckleswtf/scribe

php artisan vendor:publish --tag=scribe-config
php artisan scribe:generate

在 Controller 上加入註解:

/**
 * 取得商品列表
 *
 * @queryParam category_id integer 分類 ID. Example: 1
 * @queryParam search string 搜尋關鍵字. Example: iPhone
 * @queryParam per_page integer 每頁筆數. Example: 15
 */
public function index(Request $request)
{
    // ...
}

速率限制

// app/Providers/RouteServiceProvider.php
RateLimiter::for('api', function (Request $request) {
    return Limit::perMinute(60)->by(
        $request->user()?->id ?: $request->ip()
    );
});

結語

好的 API 設計需要考慮一致性、可讀性和可擴展性。遵循 RESTful 原則、統一回應格式、做好錯誤處理,這些看似基本的事情做好了,前後端協作會順暢很多,系統也更容易維護。希望這篇文章對你有幫助。

#Laravel #API #RESTful #後端開發
Thousand Lab

Thousand Lab