Laravel เหมาะสำหรับการพัฒนา REST API เพราะมีเครื่องมือสำหรับ Routing, Validation, Eloquent ORM, API Resource, Authentication, Rate Limiting และ Testing อยู่ใน framework เดียวกัน

บทความนี้ใช้แนวทางของ Laravel 13 และสร้างตัวอย่าง Product API พร้อมทำ API Versioning แบบ URL Path ดังนี้

/api/v1/products
/api/v2/products

แนวทางนี้เหมาะกับ Public API และระบบที่ต้องรองรับ Client หลายรุ่น เช่น Web Application, Mobile Application และระบบภายนอกที่เชื่อมต่อผ่าน API


#1. API Versioning คืออะไร

API Versioning คือการกำหนดเวอร์ชันให้ API เพื่อให้เราสามารถปรับปรุงโครงสร้าง Request, Response หรือ Business Logic โดยไม่ทำให้ Client รุ่นเดิมหยุดทำงาน

ตัวอย่าง

GET /api/v1/products
GET /api/v2/products

สมมติว่า v1 ส่งข้อมูลดังนี้

{
  "id": 1,
  "name": "Mechanical Keyboard",
  "price": 2500
}

แต่ v2 ต้องการเปลี่ยนรูปแบบ Response

{
  "id": 1,
  "name": "Mechanical Keyboard",
  "pricing": {
    "amount": 2500,
    "currency": "THB"
  }
}

ถ้าแก้ Response ของ v1 โดยตรง Client เดิมอาจเกิดปัญหา ดังนั้นเราสามารถสร้าง v2 ขึ้นมาโดยยังคง v1 ไว้ในช่วงเปลี่ยนผ่าน


#2. รูปแบบ API Versioning ที่นิยม

#2.1 URL Path Versioning

/api/v1/products
/api/v2/products

ข้อดี

  • เข้าใจง่าย
  • Debug ง่าย
  • แยก Route ได้ชัดเจน
  • เหมาะกับ Public API
  • ใช้งานกับ API Gateway และ Documentation ได้สะดวก

ตัวอย่างใน Laravel

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

#2.2 Header Versioning

ตัวอย่าง

GET /api/products
Accept: application/vnd.example.v2+json

ข้อดีคือ URL ไม่เปลี่ยน แต่มีความซับซ้อนในการทดสอบและ Debug มากกว่า URL Path Versioning

#2.3 Query Parameter Versioning

GET /api/products?version=2

ทำได้ง่าย แต่โดยทั่วไปไม่ใช่ตัวเลือกแรกสำหรับ API ขนาดใหญ่ เพราะ Route และ Cache Policy อาจจัดการยากขึ้น

สำหรับ Workshop นี้จะใช้ URL Path Versioning เพราะอ่านง่ายและดูแล Route ได้ชัดเจน


#Workshop: Product REST API

#3. สร้าง Laravel Project

ติดตั้ง Laravel Installer หากยังไม่มี

composer global require laravel/installer

สร้าง Project

laravel new laravel-api-versioning
cd laravel-api-versioning

ตรวจสอบเวอร์ชัน

php artisan --version

#4. เปิดใช้งาน API Routes

Laravel รุ่นปัจจุบันสามารถเปิด API routing ด้วยคำสั่ง

php artisan install:api

คำสั่งนี้จะสร้าง

routes/api.php

และติดตั้ง Laravel Sanctum สำหรับ Token Authentication ด้วย

Route ใน routes/api.php จะได้รับ prefix /api อัตโนมัติ

ดังนั้น

Route::get('/products', ...);

จะกลายเป็น

/api/products

#5. ตั้งค่าฐานข้อมูล

ตัวอย่าง .env

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel_api
DB_USERNAME=root
DB_PASSWORD=

สร้างฐานข้อมูล

CREATE DATABASE laravel_api;

#6. สร้าง Product Model และ Migration

php artisan make:model Product -m

แก้ไข Migration

Schema::create('products', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->text('description')->nullable();
    $table->decimal('price', 10, 2);
    $table->unsignedInteger('stock')->default(0);
    $table->timestamps();
});

รัน Migration

php artisan migrate

#7. กำหนด Product Model

ไฟล์

app/Models/Product.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    protected $fillable = [
        'name',
        'description',
        'price',
        'stock',
    ];
}

#API Version 1

#8. สร้าง Controller สำหรับ v1

สร้างโฟลเดอร์แยกตาม API Version

mkdir -p app/Http/Controllers/Api/V1

สร้าง Controller

php artisan make:controller Api/V1/ProductController --api

โครงสร้าง

app/
└── Http/
    └── Controllers/
        └── Api/
            └── V1/
                └── ProductController.php

#9. สร้าง Form Request

php artisan make:request StoreProductRequest
php artisan make:request UpdateProductRequest

#StoreProductRequest

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class StoreProductRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'name' => ['required', 'string', 'max:255'],
            'description' => ['nullable', 'string'],
            'price' => ['required', 'numeric', 'min:0'],
            'stock' => ['required', 'integer', 'min:0'],
        ];
    }
}

#UpdateProductRequest

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class UpdateProductRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'name' => ['sometimes', 'required', 'string', 'max:255'],
            'description' => ['sometimes', 'nullable', 'string'],
            'price' => ['sometimes', 'required', 'numeric', 'min:0'],
            'stock' => ['sometimes', 'required', 'integer', 'min:0'],
        ];
    }
}

#10. สร้าง API Resource สำหรับ v1

php artisan make:resource Api/V1/ProductResource

ไฟล์

app/Http/Resources/Api/V1/ProductResource.php
<?php

namespace App\Http\Resources\Api\V1;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class ProductResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'description' => $this->description,
            'price' => (float) $this->price,
            'stock' => $this->stock,
        ];
    }
}

API Resource ทำหน้าที่เป็น Transformation Layer ระหว่าง Eloquent Model กับ JSON Response ช่วยลดการผูก Response API กับโครงสร้างฐานข้อมูลโดยตรง


#11. ProductController v1

<?php

namespace App\Http\Controllers\Api\V1;

use App\Http\Controllers\Controller;
use App\Http\Requests\StoreProductRequest;
use App\Http\Requests\UpdateProductRequest;
use App\Http\Resources\Api\V1\ProductResource;
use App\Models\Product;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Http\Response;

class ProductController extends Controller
{
    public function index(): AnonymousResourceCollection
    {
        $products = Product::query()
            ->latest()
            ->paginate(10);

        return ProductResource::collection($products);
    }

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

        return new ProductResource($product);
    }

    public function show(Product $product): ProductResource
    {
        return new ProductResource($product);
    }

    public function update(
        UpdateProductRequest $request,
        Product $product
    ): ProductResource {
        $product->update($request->validated());

        return new ProductResource($product->refresh());
    }

    public function destroy(Product $product): Response
    {
        $product->delete();

        return response()->noContent();
    }
}

#12. กำหนด Route v1

แก้ไข

routes/api.php
<?php

use App\Http\Controllers\Api\V1\ProductController as ProductControllerV1;
use Illuminate\Support\Facades\Route;

Route::prefix('v1')
    ->name('api.v1.')
    ->group(function () {
        Route::apiResource('products', ProductControllerV1::class);
    });

ตรวจสอบ Route

php artisan route:list --path=api

จะได้ Endpoint หลักประมาณนี้

Method Endpoint Action
GET /api/v1/products index
POST /api/v1/products store
GET /api/v1/products/{product} show
PUT/PATCH /api/v1/products/{product} update
DELETE /api/v1/products/{product} destroy

#ทดสอบ API v1

#13. Create Product

curl -X POST http://localhost:8000/api/v1/products \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Mechanical Keyboard",
    "description": "75 percent keyboard",
    "price": 2500,
    "stock": 20
  }'

#14. Get Products

curl \
  -H "Accept: application/json" \
  http://localhost:8000/api/v1/products

#15. Get Product

curl \
  -H "Accept: application/json" \
  http://localhost:8000/api/v1/products/1

#16. Update Product

curl -X PATCH http://localhost:8000/api/v1/products/1 \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "price": 2790
  }'

#17. Delete Product

curl -X DELETE \
  -H "Accept: application/json" \
  http://localhost:8000/api/v1/products/1

#API Version 2

สมมติว่า API v2 ต้องการ

  1. เปลี่ยนโครงสร้างราคาเป็น pricing
  2. เพิ่ม availability
  3. เพิ่ม created_at
  4. ยังคง v1 ให้ Client เดิมใช้งานได้

#18. สร้าง Controller v2

php artisan make:controller Api/V2/ProductController --api

โครงสร้าง

app/Http/Controllers/Api/
├── V1/
│   └── ProductController.php
└── V2/
    └── ProductController.php

#19. สร้าง ProductResource v2

php artisan make:resource Api/V2/ProductResource
<?php

namespace App\Http\Resources\Api\V2;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class ProductResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,

            'name' => $this->name,

            'description' => $this->description,

            'pricing' => [
                'amount' => (float) $this->price,
                'currency' => 'THB',
            ],

            'inventory' => [
                'stock' => $this->stock,
                'availability' => $this->stock > 0
                    ? 'in_stock'
                    : 'out_of_stock',
            ],

            'created_at' => $this->created_at?->toISOString(),
        ];
    }
}

#20. ProductController v2

<?php

namespace App\Http\Controllers\Api\V2;

use App\Http\Controllers\Controller;
use App\Http\Requests\StoreProductRequest;
use App\Http\Requests\UpdateProductRequest;
use App\Http\Resources\Api\V2\ProductResource;
use App\Models\Product;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Http\Response;

class ProductController extends Controller
{
    public function index(): AnonymousResourceCollection
    {
        return ProductResource::collection(
            Product::query()
                ->latest()
                ->paginate(20)
        );
    }

    public function store(StoreProductRequest $request): ProductResource
    {
        return new ProductResource(
            Product::create($request->validated())
        );
    }

    public function show(Product $product): ProductResource
    {
        return new ProductResource($product);
    }

    public function update(
        UpdateProductRequest $request,
        Product $product
    ): ProductResource {
        $product->update($request->validated());

        return new ProductResource($product->refresh());
    }

    public function destroy(Product $product): Response
    {
        $product->delete();

        return response()->noContent();
    }
}

#21. เพิ่ม Route v2

<?php

use App\Http\Controllers\Api\V1\ProductController as ProductControllerV1;
use App\Http\Controllers\Api\V2\ProductController as ProductControllerV2;
use Illuminate\Support\Facades\Route;

Route::prefix('v1')
    ->name('api.v1.')
    ->group(function () {
        Route::apiResource('products', ProductControllerV1::class);
    });

Route::prefix('v2')
    ->name('api.v2.')
    ->group(function () {
        Route::apiResource('products', ProductControllerV2::class);
    });

ตอนนี้ระบบรองรับพร้อมกัน

GET /api/v1/products
GET /api/v2/products

#22. เปรียบเทียบ Response v1 กับ v2

#v1

{
  "data": {
    "id": 1,
    "name": "Mechanical Keyboard",
    "description": "75 percent keyboard",
    "price": 2500,
    "stock": 20
  }
}

#v2

{
  "data": {
    "id": 1,
    "name": "Mechanical Keyboard",
    "description": "75 percent keyboard",
    "pricing": {
      "amount": 2500,
      "currency": "THB"
    },
    "inventory": {
      "stock": 20,
      "availability": "in_stock"
    },
    "created_at": "2026-09-24T01:00:00.000000Z"
  }
}

จุดสำคัญคือฐานข้อมูลสามารถเป็นชุดเดียวกัน แต่แต่ละ API Version มี Transformation Layer คนละชุด


#23. แยก Route File ตาม Version

เมื่อ API มีจำนวน Endpoint มาก ไม่ควรรวมทุก Version ไว้ใน routes/api.php

โครงสร้างที่ดูแลง่ายกว่า

routes/
├── api.php
└── api/
    ├── v1.php
    └── v2.php

routes/api.php

<?php

use Illuminate\Support\Facades\Route;

Route::prefix('v1')
    ->name('api.v1.')
    ->group(base_path('routes/api/v1.php'));

Route::prefix('v2')
    ->name('api.v2.')
    ->group(base_path('routes/api/v2.php'));

routes/api/v1.php

<?php

use App\Http\Controllers\Api\V1\ProductController;
use Illuminate\Support\Facades\Route;

Route::apiResource('products', ProductController::class);

routes/api/v2.php

<?php

use App\Http\Controllers\Api\V2\ProductController;
use Illuminate\Support\Facades\Route;

Route::apiResource('products', ProductController::class);

วิธีนี้เหมาะกับระบบที่มีหลาย Domain เช่น

/api/v1/users
/api/v1/products
/api/v1/orders
/api/v1/payments

/api/v2/users
/api/v2/products
/api/v2/orders
/api/v2/payments

#24. โครงสร้าง Project ที่แนะนำ

app/
├── Http/
│   ├── Controllers/
│   │   └── Api/
│   │       ├── V1/
│   │       │   └── ProductController.php
│   │       └── V2/
│   │           └── ProductController.php
│   │
│   ├── Requests/
│   │   ├── StoreProductRequest.php
│   │   └── UpdateProductRequest.php
│   │
│   └── Resources/
│       └── Api/
│           ├── V1/
│           │   └── ProductResource.php
│           └── V2/
│               └── ProductResource.php
│
├── Models/
│   └── Product.php
│
routes/
├── api.php
└── api/
    ├── v1.php
    └── v2.php

ถ้า Business Logic ซับซ้อน ควรแยกเพิ่ม

app/
├── Actions/
├── Services/
├── Repositories/
├── DTOs/
└── Domain/

เพื่อไม่ให้ Controller ของแต่ละ Version มี Business Logic ซ้ำกัน


#25. หลักการสำคัญ: Version ที่ API Contract ไม่ใช่ฐานข้อมูล

สิ่งที่ควรเปลี่ยนตาม Version ส่วนใหญ่คือ

Route
Controller / Adapter
Request Contract
Response Resource
Serialization
Validation บางส่วน

แต่ Business Logic หลักควรถูก reuse

ตัวอย่าง

API v1 Controller ─┐
                   ├── ProductService ── Product Model ── Database
API v2 Controller ─┘

อย่าทำแบบนี้โดยไม่จำเป็น

ProductV1 Model
ProductV2 Model
ProductV1 Database Table
ProductV2 Database Table

เพราะ API Version ไม่ควรถูกผูกโดยตรงกับ Database Version


#26. Service Layer เพื่อ Share Business Logic

สร้าง

app/Services/ProductService.php
<?php

namespace App\Services;

use App\Models\Product;

class ProductService
{
    public function create(array $data): Product
    {
        return Product::create($data);
    }

    public function update(Product $product, array $data): Product
    {
        $product->update($data);

        return $product->refresh();
    }
}

จากนั้นทั้ง v1 และ v2 ใช้ Service เดียวกันได้

public function __construct(
    private ProductService $productService
) {
}

แนวคิดคือ

Version-specific HTTP Contract
        ↓
Shared Application / Domain Logic
        ↓
Shared Persistence Layer

#27. Authentication ด้วย Laravel Sanctum

หลังจาก

php artisan install:api

สามารถป้องกัน API ด้วย auth:sanctum

Route::prefix('v1')
    ->middleware('auth:sanctum')
    ->group(function () {
        Route::apiResource('products', ProductController::class);
    });

หรือกำหนดเฉพาะ Endpoint

Route::get('/products', [ProductController::class, 'index']);

Route::middleware('auth:sanctum')->group(function () {
    Route::post('/products', [ProductController::class, 'store']);
    Route::patch('/products/{product}', [ProductController::class, 'update']);
    Route::delete('/products/{product}', [ProductController::class, 'destroy']);
});

#28. Versioning กับ Middleware

บางระบบต้องการ Middleware เฉพาะ Version

Route::prefix('v1')
    ->middleware([
        'auth:sanctum',
        'throttle:api',
    ])
    ->group(function () {
        // v1
    });

Route::prefix('v2')
    ->middleware([
        'auth:sanctum',
        'throttle:api',
    ])
    ->group(function () {
        // v2
    });

สามารถใช้ Middleware สำหรับ

  • Authentication
  • Authorization
  • Rate Limiting
  • Logging
  • API Deprecation Header
  • Tenant Resolution
  • Request Correlation ID

#29. การประกาศ Deprecation

เมื่อ v2 พร้อมใช้งานแล้ว ไม่ควรปิด v1 ทันที

Lifecycle ที่เหมาะสม

v1 Active
   ↓
v2 Released
   ↓
v1 Deprecated
   ↓
Migration Period
   ↓
v1 Sunset
   ↓
v1 Removed

ระหว่าง Deprecated สามารถเพิ่ม HTTP Header เช่น

Deprecation: true
Sunset: Thu, 31 Dec 2026 23:59:59 GMT
Link: </docs/migration/v1-to-v2>; rel="deprecation"

และแจ้ง Client ล่วงหน้าอย่างชัดเจน


#30. เมื่อใดควรเพิ่ม API Version ใหม่

ควรพิจารณา Major Version ใหม่เมื่อมี Breaking Change เช่น

#เปลี่ยนชื่อ Field

จาก

{
  "price": 2500
}

เป็น

{
  "amount": 2500
}

#ลบ Field

จาก

{
  "id": 1,
  "name": "Keyboard",
  "legacy_code": "KB001"
}

เป็น

{
  "id": 1,
  "name": "Keyboard"
}

#เปลี่ยน Data Type

จาก

{
  "price": "2500"
}

เป็น

{
  "price": 2500
}

#เปลี่ยนโครงสร้าง Response

จาก

{
  "price": 2500,
  "currency": "THB"
}

เป็น

{
  "pricing": {
    "amount": 2500,
    "currency": "THB"
  }
}

#31. การเปลี่ยนแปลงที่อาจไม่ต้องสร้าง Version ใหม่

Backward-compatible change เช่น

#เพิ่ม Optional Field

{
  "id": 1,
  "name": "Keyboard",
  "description": "..."
}

#เพิ่ม Endpoint ใหม่

GET /api/v1/categories

#เพิ่ม Optional Query Parameter

GET /api/v1/products?category=keyboard

ทั้งนี้ควรตรวจสอบ Contract ที่ตกลงกับ Client ก่อนเสมอ


#32. API Error Response

ควรมีรูปแบบ Error ที่คงที่

ตัวอย่าง

{
  "message": "Validation failed",
  "errors": {
    "price": [
      "The price field must be at least 0."
    ]
  }
}

สำหรับ Domain Error อาจกำหนดมาตรฐานเอง

{
  "error": {
    "code": "PRODUCT_OUT_OF_STOCK",
    "message": "Product is out of stock"
  }
}

ถ้ามี Client หลายระบบ ควรถือ Error Contract เป็นส่วนหนึ่งของ API Versioning ด้วย


#33. API Pagination

ตัวอย่าง

Product::query()->paginate(20);

เมื่อใช้กับ Resource

return ProductResource::collection(
    Product::query()->paginate(20)
);

Response จะมีข้อมูล Pagination เช่น links และ meta

Client จึงไม่ควร assume ว่า Endpoint List จะคืน Array เปล่า ๆ เท่านั้น


#34. API Filtering

ตัวอย่าง

GET /api/v2/products?search=keyboard

Controller

$products = Product::query()
    ->when(
        request('search'),
        fn ($query, $search) =>
            $query->where('name', 'like', "%{$search}%")
    )
    ->paginate(20);

#35. API Sorting

GET /api/v2/products?sort=price&direction=desc

ควร whitelist Field ที่อนุญาต

$allowedSorts = [
    'name',
    'price',
    'created_at',
];

$sort = request('sort', 'created_at');

if (! in_array($sort, $allowedSorts, true)) {
    $sort = 'created_at';
}

หลีกเลี่ยงการส่งชื่อ Column จาก User เข้า orderBy() โดยไม่ตรวจสอบ


#36. API Rate Limiting

สำหรับ Public API ควรมี Rate Limiting

ตัวอย่างแนวคิด

Route::middleware('throttle:api')
    ->prefix('v1')
    ->group(function () {
        // ...
    });

สามารถกำหนด Rate Limiter เองตาม User, API Key หรือ IP ได้


#37. Testing API Version

สร้าง Feature Test แยกตาม Version

tests/
└── Feature/
    └── Api/
        ├── V1/
        │   └── ProductApiTest.php
        └── V2/
            └── ProductApiTest.php

สร้าง Test

php artisan make:test Api/V1/ProductApiTest
php artisan make:test Api/V2/ProductApiTest

ตัวอย่าง v1

public function test_can_get_product_v1(): void
{
    $product = Product::factory()->create([
        'price' => 2500,
    ]);

    $response = $this->getJson(
        "/api/v1/products/{$product->id}"
    );

    $response
        ->assertOk()
        ->assertJsonPath('data.price', 2500);
}

ตัวอย่าง v2

public function test_can_get_product_v2(): void
{
    $product = Product::factory()->create([
        'price' => 2500,
    ]);

    $response = $this->getJson(
        "/api/v2/products/{$product->id}"
    );

    $response
        ->assertOk()
        ->assertJsonPath('data.pricing.amount', 2500)
        ->assertJsonPath('data.pricing.currency', 'THB');
}

รัน Test

php artisan test

#38. Contract Test สำคัญอย่างไร

เมื่อมีหลาย API Version เป้าหมายสำคัญคือ

การเพิ่ม v2
ต้องไม่ทำให้
v1 เปลี่ยน Contract

ดังนั้นควรมี Test ที่ตรวจ

  • HTTP Status
  • Required Fields
  • JSON Structure
  • Data Type
  • Error Format
  • Authentication
  • Pagination
  • Rate Limit
  • Deprecated Endpoint

ตัวอย่าง

$response->assertJsonStructure([
    'data' => [
        'id',
        'name',
        'price',
        'stock',
    ],
]);

#39. API Documentation

ควรมี Documentation แยกตาม Version

/docs/api/v1
/docs/api/v2

ตัวอย่าง OpenAPI

openapi: 3.1.0

info:
  title: Product API
  version: 2.0.0

servers:
  - url: https://api.example.com/api/v2

หากใช้ Swagger/OpenAPI ควรให้ Specification เป็นส่วนหนึ่งของ CI/CD

แนวทาง Pipeline

Push Code
   ↓
PHPUnit / Pest
   ↓
API Contract Test
   ↓
OpenAPI Validation
   ↓
Build
   ↓
Deploy

#40. แนวทาง Versioning ที่แนะนำสำหรับ Laravel

สำหรับ API ทั่วไปสามารถใช้โครงสร้าง

/api/v1/...
/api/v2/...

และจัด Project เป็น

Controller
├── Api/V1
└── Api/V2

Resource
├── Api/V1
└── Api/V2

แต่ Business Logic ควรอยู่ใน

Service
Action
Domain
Repository

เพื่อให้ reuse ข้าม Version ได้


#41. สถาปัตยกรรมตัวอย่าง

flowchart LR

    C1[Web Client]
    C2[Mobile Client]
    C3[External Client]

    C1 --> GW[Laravel API]
    C2 --> GW
    C3 --> GW

    GW --> V1[/api/v1]
    GW --> V2[/api/v2]

    V1 --> R1[V1 Resource]
    V2 --> R2[V2 Resource]

    R1 --> S[Shared Service Layer]
    R2 --> S

    S --> M[Eloquent Model]
    M --> DB[(Database)]

#42. Request Flow

sequenceDiagram

    participant Client
    participant Route
    participant Request
    participant Controller
    participant Service
    participant Model
    participant Resource

    Client->>Route: POST /api/v2/products
    Route->>Request: Validate Request
    Request->>Controller: Valid Data
    Controller->>Service: create(data)
    Service->>Model: Product::create()
    Model-->>Service: Product
    Service-->>Controller: Product
    Controller->>Resource: ProductResource
    Resource-->>Client: JSON Response

#43. Best Practices

  1. ใช้ URL Versioning เช่น /api/v1
  2. ใช้ Route::apiResource() สำหรับ REST Resource
  3. ใช้ Form Request สำหรับ Validation
  4. ใช้ API Resource เป็น Response Contract
  5. แยก Controller/Resource ตาม Version
  6. Share Business Logic ผ่าน Service หรือ Action
  7. อย่า copy Business Logic ทั้งชุดระหว่าง v1 กับ v2
  8. เขียน Feature Test แยกแต่ละ Version
  9. รักษา Error Contract ให้สม่ำเสมอ
  10. ประกาศ Deprecated และ Sunset Policy ก่อนถอด Version เก่า
  11. มี OpenAPI Specification แยก Version
  12. ใช้ Authentication, Authorization และ Rate Limiting
  13. เปลี่ยน Major Version เฉพาะเมื่อเกิด Breaking Change
  14. รองรับ Client Migration Period อย่างชัดเจน

#44. สรุป

แนวทางที่ดูแลรักษาง่ายสำหรับ Laravel API คือ

Client
   ↓
/api/v1 ── V1 Controller ── V1 Resource ─┐
                                         ├── Shared Service ── Model ── DB
/api/v2 ── V2 Controller ── V2 Resource ─┘

หัวใจของ API Versioning ไม่ใช่เพียงการเติม v1 หรือ v2 ใน URL แต่คือการบริหาร API Contract ให้ Client เดิมยังทำงานได้ ในขณะที่ระบบสามารถพัฒนาเวอร์ชันใหม่ต่อไปได้

สำหรับ Laravel ควรใช้ Route Prefix + Versioned Controller/Resource และแยก Business Logic ส่วนกลางออกมา เพื่อหลีกเลี่ยง Code Duplication และทำให้การพัฒนา API หลาย Version มีความยั่งยืน


#References