- 1. API Versioning คืออะไร
- 2. รูปแบบ API Versioning ที่นิยม
- Workshop: Product REST API
- API Version 1
- ทดสอบ API v1
- API Version 2
- 22. เปรียบเทียบ Response v1 กับ v2
- 23. แยก Route File ตาม Version
- 24. โครงสร้าง Project ที่แนะนำ
- 25. หลักการสำคัญ: Version ที่ API Contract ไม่ใช่ฐานข้อมูล
- 26. Service Layer เพื่อ Share Business Logic
- 27. Authentication ด้วย Laravel Sanctum
- 28. Versioning กับ Middleware
- 29. การประกาศ Deprecation
- 30. เมื่อใดควรเพิ่ม API Version ใหม่
- 31. การเปลี่ยนแปลงที่อาจไม่ต้องสร้าง Version ใหม่
- 32. API Error Response
- 33. API Pagination
- 34. API Filtering
- 35. API Sorting
- 36. API Rate Limiting
- 37. Testing API Version
- 38. Contract Test สำคัญอย่างไร
- 39. API Documentation
- 40. แนวทาง Versioning ที่แนะนำสำหรับ Laravel
- 41. สถาปัตยกรรมตัวอย่าง
- 42. Request Flow
- 43. Best Practices
- 44. สรุป
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 ต้องการ
- เปลี่ยนโครงสร้างราคาเป็น
pricing - เพิ่ม
availability - เพิ่ม
created_at - ยังคง 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
- ใช้ URL Versioning เช่น
/api/v1 - ใช้
Route::apiResource()สำหรับ REST Resource - ใช้ Form Request สำหรับ Validation
- ใช้ API Resource เป็น Response Contract
- แยก Controller/Resource ตาม Version
- Share Business Logic ผ่าน Service หรือ Action
- อย่า copy Business Logic ทั้งชุดระหว่าง v1 กับ v2
- เขียน Feature Test แยกแต่ละ Version
- รักษา Error Contract ให้สม่ำเสมอ
- ประกาศ Deprecated และ Sunset Policy ก่อนถอด Version เก่า
- มี OpenAPI Specification แยก Version
- ใช้ Authentication, Authorization และ Rate Limiting
- เปลี่ยน Major Version เฉพาะเมื่อเกิด Breaking Change
- รองรับ 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
-
Laravel 13 Documentation — Routing
https://laravel.com/docs/13.x/routing -
Laravel 13 Documentation — Validation
https://laravel.com/docs/13.x/validation -
Laravel 13 Documentation — API Resources
https://laravel.com/docs/13.x/eloquent-resources -
Laravel 13 Documentation — Sanctum
https://laravel.com/docs/13.x/sanctum -
Laravel 13 Release Notes
https://laravel.com/docs/13.x/releases