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