Skip to content

🌐 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-list

HTTP Methods ​

MethodPurposeExampleResponse Code
GETRetrieve resource(s)GET /api/users200 OK
POSTCreate new resourcePOST /api/users201 Created
PUTFull updatePUT /api/users/1200 OK
PATCHPartial updatePATCH /api/users/1200 OK
DELETEDelete resourceDELETE /api/users/1204 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) ​

CodeMeaningWhen to Use
200 OKSuccessGET, PUT, PATCH requests
201 CreatedResource createdPOST requests
204 No ContentSuccess, no response bodyDELETE requests

Client Error Codes (4xx) ​

CodeMeaningWhen to Use
400 Bad RequestInvalid requestGeneral errors
401 UnauthorizedNot authenticatedMissing/invalid auth
403 ForbiddenNot authorizedInsufficient permissions
404 Not FoundResource not foundInvalid ID/route
422 Unprocessable EntityValidation failedForm validation errors
429 Too Many RequestsRate limit exceededRate limiting

Server Error Codes (5xx) ​

CodeMeaningWhen to Use
500 Internal Server ErrorServer errorUnexpected errors
503 Service UnavailableService downMaintenance mode

🔐 API Versioning ​

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.

Built with VitePress