Appearance
🌐 API Design Standards
Comprehensive guidelines for building consistent, maintainable, and well-documented RESTful APIs in Laravel.
🎯 RESTful Conventions
Resource Naming
Use plural nouns for resources:
✅ Good:
/api/users
/api/products
/api/orders
❌ Bad:
/api/user
/api/getProducts
/api/order-listHTTP Methods
| Method | Purpose | Example | Response Code |
|---|---|---|---|
GET | Retrieve resource(s) | GET /api/users | 200 OK |
POST | Create new resource | POST /api/users | 201 Created |
PUT | Full update | PUT /api/users/1 | 200 OK |
PATCH | Partial update | PATCH /api/users/1 | 200 OK |
DELETE | Delete resource | DELETE /api/users/1 | 204 No Content |
URL Patterns
# Collection
GET /api/users # List all users
POST /api/users # Create user
# Resource
GET /api/users/{id} # Get specific user
PUT /api/users/{id} # Full update user
PATCH /api/users/{id} # Partial update user
DELETE /api/users/{id} # Delete user
# Nested Resources
GET /api/users/{id}/posts # Get user's posts
POST /api/users/{id}/posts # Create post for user
# Actions (when REST doesn't fit)
POST /api/users/{id}/activate
POST /api/orders/{id}/cancel
POST /api/posts/{id}/publish📦 Response Format Macros
Complete Implementation
For a comprehensive, production-ready ResponseMacroServiceProvider with full JsonResource and ResourceCollection integration, see the dedicated Response Macro Service Provider guide.
Standard Response Macro
Define consistent response structures using macros:
php
<?php
// app/Providers/ResponseMacroServiceProvider.php
declare(strict_types=1);
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Response as ResponseFacade;
class ResponseMacroServiceProvider extends ServiceProvider
{
public function boot(): void
{
$this->registerSuccessResponse();
$this->registerErrorResponse();
$this->registerValidationErrorResponse();
$this->registerNotFoundResponse();
$this->registerUnauthorizedResponse();
$this->registerForbiddenResponse();
}
private function registerSuccessResponse(): void
{
ResponseFacade::macro('success', function ($data = null, string $message = 'Success', int $code = 200) {
return ResponseFacade::json([
'success' => true,
'message' => $message,
'data' => $data,
], $code);
});
}
private function registerErrorResponse(): void
{
ResponseFacade::macro('error', function (
string $message = 'An error occurred',
?string $errorCode = null,
int $statusCode = 400,
?array $errors = null
) {
$response = [
'success' => false,
'error' => [
'message' => $message,
'code' => $errorCode ?? strtoupper(str_replace(' ', '_', $message)),
],
];
if ($errors) {
$response['error']['details'] = $errors;
}
return ResponseFacade::json($response, $statusCode);
});
}
private function registerValidationErrorResponse(): void
{
ResponseFacade::macro('validationError', function (array $errors, string $message = 'Validation failed') {
return ResponseFacade::json([
'success' => false,
'error' => [
'message' => $message,
'code' => 'VALIDATION_ERROR',
'details' => $errors,
],
], 422);
});
}
private function registerNotFoundResponse(): void
{
ResponseFacade::macro('notFound', function (string $resource = 'Resource', string $identifier = '') {
$message = $identifier
? "{$resource} with identifier '{$identifier}' not found"
: "{$resource} not found";
return ResponseFacade::json([
'success' => false,
'error' => [
'message' => $message,
'code' => 'RESOURCE_NOT_FOUND',
],
], 404);
});
}
private function registerUnauthorizedResponse(): void
{
ResponseFacade::macro('unauthorized', function (string $message = 'Unauthorized') {
return ResponseFacade::json([
'success' => false,
'error' => [
'message' => $message,
'code' => 'UNAUTHORIZED',
],
], 401);
});
}
private function registerForbiddenResponse(): void
{
ResponseFacade::macro('forbidden', function (string $message = 'Forbidden') {
return ResponseFacade::json([
'success' => false,
'error' => [
'message' => $message,
'code' => 'FORBIDDEN',
],
], 403);
});
}
}Register the Service Provider
php
// config/app.php
'providers' => [
// Other providers...
App\Providers\ResponseMacroServiceProvider::class,
],Using Response Macros
php
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreUserRequest;
use App\Http\Resources\UserResource;
use App\Services\UserService;
use Illuminate\Http\JsonResponse;
class UserController extends Controller
{
public function __construct(
private UserService $userService
) {}
/**
* List all users
*/
public function index(): JsonResponse
{
$users = $this->userService->getAllUsers();
return response()->success(
UserResource::collection($users),
'Users retrieved successfully'
);
}
/**
* Create a new user
*/
public function store(StoreUserRequest $request): JsonResponse
{
$user = $this->userService->createUser($request->validated());
return response()->success(
new UserResource($user),
'User created successfully',
201
);
}
/**
* Get specific user
*/
public function show(int $id): JsonResponse
{
try {
$user = $this->userService->getUserById($id);
return response()->success(
new UserResource($user),
'User retrieved successfully'
);
} catch (UserNotFoundException $e) {
return response()->notFound('User', (string) $id);
}
}
/**
* Update user
*/
public function update(UpdateUserRequest $request, int $id): JsonResponse
{
try {
$user = $this->userService->updateUser($id, $request->validated());
return response()->success(
new UserResource($user),
'User updated successfully'
);
} catch (UserNotFoundException $e) {
return response()->notFound('User', (string) $id);
}
}
/**
* Delete user
*/
public function destroy(int $id): JsonResponse
{
try {
$this->userService->deleteUser($id);
return response()->success(null, 'User deleted successfully', 204);
} catch (UserNotFoundException $e) {
return response()->notFound('User', (string) $id);
}
}
}📊 Standard Response Formats
Success Response
json
{
"success": true,
"message": "Operation successful",
"data": {
"id": 1,
"name": "John Doe",
"email": "[email protected]"
}
}Collection Response
json
{
"success": true,
"message": "Users retrieved successfully",
"data": [
{
"id": 1,
"name": "John Doe",
"email": "[email protected]"
},
{
"id": 2,
"name": "Jane Smith",
"email": "[email protected]"
}
],
"meta": {
"current_page": 1,
"per_page": 15,
"total": 100,
"last_page": 7
}
}Error Response
json
{
"success": false,
"error": {
"message": "User not found",
"code": "RESOURCE_NOT_FOUND"
}
}Validation Error Response
json
{
"success": false,
"error": {
"message": "Validation failed",
"code": "VALIDATION_ERROR",
"details": {
"email": [
"The email field is required.",
"The email must be a valid email address."
],
"password": [
"The password must be at least 12 characters."
]
}
}
}🔢 HTTP Status Codes
Success Codes (2xx)
| Code | Meaning | When to Use |
|---|---|---|
200 OK | Success | GET, PUT, PATCH requests |
201 Created | Resource created | POST requests |
204 No Content | Success, no response body | DELETE requests |
Client Error Codes (4xx)
| Code | Meaning | When to Use |
|---|---|---|
400 Bad Request | Invalid request | General errors |
401 Unauthorized | Not authenticated | Missing/invalid auth |
403 Forbidden | Not authorized | Insufficient permissions |
404 Not Found | Resource not found | Invalid ID/route |
422 Unprocessable Entity | Validation failed | Form validation errors |
429 Too Many Requests | Rate limit exceeded | Rate limiting |
Server Error Codes (5xx)
| Code | Meaning | When to Use |
|---|---|---|
500 Internal Server Error | Server error | Unexpected errors |
503 Service Unavailable | Service down | Maintenance mode |
🔐 API Versioning
URL Versioning (Recommended)
php
// routes/api.php
Route::prefix('v1')->group(function () {
Route::apiResource('users', UserController::class);
Route::apiResource('posts', PostController::class);
});
Route::prefix('v2')->group(function () {
Route::apiResource('users', V2\UserController::class);
Route::apiResource('posts', V2\PostController::class);
});Directory Structure
app/Http/Controllers/Api/
├── V1/
│ ├── UserController.php
│ └── PostController.php
└── V2/
├── UserController.php
└── PostController.php📄 Pagination
Standard Pagination Format
php
<?php
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
class UserController extends Controller
{
public function index(Request $request): JsonResponse
{
$perPage = min($request->get('per_page', 15), 100); // Max 100
$users = $this->userService->getPaginatedUsers($perPage);
return response()->success(
UserResource::collection($users),
'Users retrieved successfully'
);
}
}Response Format
json
{
"success": true,
"message": "Users retrieved successfully",
"data": [...],
"meta": {
"current_page": 1,
"from": 1,
"last_page": 10,
"per_page": 15,
"to": 15,
"total": 150
},
"links": {
"first": "http://api.example.com/v1/users?page=1",
"last": "http://api.example.com/v1/users?page=10",
"prev": null,
"next": "http://api.example.com/v1/users?page=2"
}
}🔍 Filtering & Sorting
Implementation
php
<?php
class UserController extends Controller
{
public function index(Request $request): JsonResponse
{
$filters = [
'search' => $request->get('search'),
'role' => $request->get('role'),
'is_active' => $request->get('is_active'),
];
$sort = [
'field' => $request->get('sort', 'created_at'),
'direction' => $request->get('order', 'desc'),
];
$users = $this->userService->getUsers($filters, $sort);
return response()->success(
UserResource::collection($users),
'Users retrieved successfully'
);
}
}Service Implementation
php
<?php
class UserService
{
public function getUsers(array $filters, array $sort): LengthAwarePaginator
{
$query = $this->userRepository->query();
// Apply filters
if (!empty($filters['search'])) {
$query->where(function ($q) use ($filters) {
$q->where('name', 'LIKE', "%{$filters['search']}%")
->orWhere('email', 'LIKE', "%{$filters['search']}%");
});
}
if (!empty($filters['role'])) {
$query->where('role', $filters['role']);
}
if (isset($filters['is_active'])) {
$query->where('is_active', (bool) $filters['is_active']);
}
// Apply sorting
$allowedSortFields = ['name', 'email', 'created_at', 'updated_at'];
$sortField = in_array($sort['field'], $allowedSortFields)
? $sort['field']
: 'created_at';
$sortDirection = in_array($sort['direction'], ['asc', 'desc'])
? $sort['direction']
: 'desc';
$query->orderBy($sortField, $sortDirection);
return $query->paginate(15);
}
}Example API Calls
bash
# Basic list
GET /api/v1/users
# With pagination
GET /api/v1/users?page=2&per_page=20
# With search
GET /api/v1/users?search=john
# With filters
GET /api/v1/users?role=admin&is_active=1
# With sorting
GET /api/v1/users?sort=name&order=asc
# Combined
GET /api/v1/users?search=john&role=admin&sort=created_at&order=desc&page=1&per_page=25🚦 Rate Limiting
Configuration
php
// app/Providers/RouteServiceProvider.php
protected function configureRateLimiting(): void
{
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
RateLimiter::for('api-strict', function (Request $request) {
return Limit::perMinute(10)->by($request->user()?->id ?: $request->ip());
});
}Apply to Routes
php
// routes/api.php
Route::middleware(['throttle:api'])->group(function () {
Route::get('/users', [UserController::class, 'index']);
});
Route::middleware(['throttle:api-strict'])->group(function () {
Route::post('/users', [UserController::class, 'store']);
});Rate Limit Response
json
{
"success": false,
"error": {
"message": "Too many requests. Please try again later.",
"code": "RATE_LIMIT_EXCEEDED"
}
}🔒 Authentication
API Token Authentication
php
// routes/api.php
Route::post('/auth/login', [AuthController::class, 'login']);
Route::post('/auth/register', [AuthController::class, 'register']);
Route::middleware('auth:sanctum')->group(function () {
Route::get('/auth/me', [AuthController::class, 'me']);
Route::post('/auth/logout', [AuthController::class, 'logout']);
Route::apiResource('users', UserController::class);
});Authentication Controller
php
<?php
class AuthController extends Controller
{
public function login(LoginRequest $request): JsonResponse
{
$user = User::where('email', $request->email)->first();
if (!$user || !Hash::check($request->password, $user->password)) {
return response()->unauthorized('Invalid credentials');
}
$token = $user->createToken('api-token')->plainTextToken;
return response()->success([
'user' => new UserResource($user),
'token' => $token,
], 'Login successful');
}
public function logout(Request $request): JsonResponse
{
$request->user()->currentAccessToken()->delete();
return response()->success(null, 'Logged out successfully');
}
}📚 API Documentation
OpenAPI/Swagger Annotations
php
<?php
use OpenApi\Attributes as OA;
#[OA\Info(version: "1.0.0", title: "My API")]
class Controller
{
// Base controller
}
#[OA\Schema(
schema: "User",
properties: [
new OA\Property(property: "id", type: "integer", example: 1),
new OA\Property(property: "name", type: "string", example: "John Doe"),
new OA\Property(property: "email", type: "string", format: "email", example: "[email protected]"),
]
)]
class UserResource extends JsonResource
{
// Resource
}
class UserController extends Controller
{
#[OA\Get(
path: "/api/v1/users",
summary: "Get list of users",
tags: ["Users"],
parameters: [
new OA\Parameter(name: "page", in: "query", required: false, schema: new OA\Schema(type: "integer")),
new OA\Parameter(name: "per_page", in: "query", required: false, schema: new OA\Schema(type: "integer")),
],
responses: [
new OA\Response(response: 200, description: "Success"),
new OA\Response(response: 401, description: "Unauthorized"),
]
)]
public function index(): JsonResponse
{
// Implementation
}
}✅ API Best Practices Checklist
- [ ] Use RESTful conventions for resource naming
- [ ] Implement consistent response formats using macros
- [ ] Version your API from the beginning
- [ ] Provide pagination for collections
- [ ] Support filtering and sorting
- [ ] Implement rate limiting
- [ ] Use proper HTTP status codes
- [ ] Implement authentication/authorization
- [ ] Document your API (OpenAPI/Swagger)
- [ ] Use API resources for response transformation
- [ ] Validate all inputs with Form Requests
- [ ] Handle errors consistently
- [ ] Return meaningful error messages
- [ ] Use HTTPS in production
- [ ] Implement CORS properly
📝 API Evolution: APIs are contracts with your clients. Version carefully and maintain backward compatibility whenever possible.