RESTful API Design in Laravel: Principles and Practices
API Design Philosophy
A well-designed API is a joy to work with. It's consistent, predictable, and self-documenting. Poor API design, on the other hand, leads to frustrated developers, bugs, and integration headaches. Getting it right from the start saves countless hours down the road.
Resource-Oriented Design
REST APIs are built around resources, not actions. URLs identify resources; HTTP methods define actions:
// Good: Resource-oriented
GET /api/orders # List orders
POST /api/orders # Create order
GET /api/orders/{id} # Get order
PUT /api/orders/{id} # Update order
DELETE /api/orders/{id} # Delete order
// Bad: Action-oriented
POST /api/getOrders
POST /api/createOrder
POST /api/deleteOrder
API Resources for Consistent Output
Laravel's API Resources provide a transformation layer between your models and JSON responses:
class OrderResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'status' => $this->status,
'total' => [
'amount' => $this->total,
'formatted' => Number::currency($this->total),
],
'items' => OrderItemResource::collection($this->whenLoaded('items')),
'customer' => new CustomerResource($this->whenLoaded('customer')),
'created_at' => $this->created_at->toIso8601String(),
'links' => [
'self' => route('api.orders.show', $this),
'invoice' => route('api.orders.invoice', $this),
],
];
}
}
Consistent Error Responses
Error responses should be consistent and informative:
class Handler extends ExceptionHandler
{
public function render($request, Throwable $e): Response
{
if ($request->expectsJson()) {
return $this->handleApiException($request, $e);
}
return parent::render($request, $e);
}
protected function handleApiException($request, Throwable $e): JsonResponse
{
$status = match (true) {
$e instanceof NotFoundHttpException => 404,
$e instanceof ValidationException => 422,
$e instanceof AuthenticationException => 401,
$e instanceof AuthorizationException => 403,
default => 500,
};
return response()->json([
'error' => [
'code' => $status,
'message' => $e->getMessage(),
'details' => $e instanceof ValidationException ? $e->errors() : null,
],
], $status);
}
}
Versioning Strategies
APIs evolve, and you need a strategy for managing changes without breaking existing clients:
// URL versioning (most common)
Route::prefix('api/v1')->group(function () {
Route::apiResource('orders', OrderController::class);
});
Route::prefix('api/v2')->group(function () {
Route::apiResource('orders', V2\OrderController::class);
});
Rate Limiting
Protect your API from abuse with rate limiting:
RateLimiter::for('api', function (Request $request) {
return $request->user()
? Limit::perMinute(100)->by($request->user()->id)
: Limit::perMinute(20)->by($request->ip());
});
Authentication with Sanctum
For most APIs, Laravel Sanctum provides simple, secure token authentication:
public function login(LoginRequest $request): JsonResponse
{
$user = User::where('email', $request->email)->first();
if (!$user || !Hash::check($request->password, $user->password)) {
throw ValidationException::withMessages([
'email' => ['The provided credentials are incorrect.'],
]);
}
return response()->json([
'token' => $user->createToken($request->device_name)->plainTextToken,
'user' => new UserResource($user),
]);
}
Documentation
Document your API. Tools like Scribe can generate documentation from your code:
/**
* @group Orders
*
* @authenticated
*
* @queryParam status string Filter by status. Example: pending
* @queryParam per_page int Items per page. Example: 20
*
* @response {
* "data": [{"id": 1, "status": "pending", "total": {"amount": 99.99}}]
* }
*/
public function index(Request $request): OrderCollection
{
// ...
}
Conclusion
Good API design is an investment that pays dividends in developer experience and system reliability. Be consistent, be predictable, and document everything.
Related Articles
Need Help With Your Project?
I respond to all inquiries within 24 hours. Let's discuss how I can help build your production-ready system.
Get In Touch