Back to blog
Laravel
Intermediate

Building Scalable REST APIs with Laravel 11 & PHP 8.3

This guide walks you through creating production‑ready REST APIs with Laravel 11 on PHP 8.3. You'll master routing, resources, authentication, testing, and deployment techniques that scale.

August 27, 202519 min read

Introduction

Building a REST API that can handle thousands of requests per second while remaining maintainable is a core skill for modern PHP developers. Laravel 11, paired with PHP 8.3, introduces performance improvements, stricter typing, and new language features that make API development faster and more reliable. In this article you will learn the full lifecycle of a production‑grade API: from architectural decisions and routing strategies to authentication, testing, documentation, and deployment.

We assume you have a working knowledge of Laravel basics, Composer, and the command line. The examples target Laravel 11.x and PHP 8.3, but most concepts apply to earlier versions with minor adjustments.

Table of Contents

Core Concepts

REST Principles

Representational State Transfer (REST) defines a set of constraints that make APIs predictable and cacheable. The key principles are:

  • Statelessness: Each request contains all information needed to process it; the server stores no session state.
  • Resource‑Based URLs: Nouns represent resources (e.g., /api/v1/users), not verbs.
  • HTTP Methods: GET, POST, PUT/PATCH, DELETE map to CRUD operations.
  • Standard Status Codes: Use 2xx for success, 4xx for client errors, 5xx for server errors.
  • Hypermedia (HATEOAS) (optional): Include links to related resources for discoverability.

Laravel API Resources

API Resources transform Eloquent models into JSON structures. They give you a single place to shape output, handle conditional fields, and embed relationships. Laravel 11 ships with JsonResource and ResourceCollection that support PHP 8.3 typed properties and the new #[Attribute] syntax for metadata.

Versioning Strategies

API versioning prevents breaking changes for existing consumers. Common approaches:

  • URL versioning: /api/v1/... (simplest, most visible).
  • Header versioning: Accept: application/vnd.myapp.v1+json (cleaner URLs).
  • Query parameter: ?version=1 (least recommended).

We recommend URL versioning for public APIs because it is explicit and works well with CDN caching.

Architecture Overview

A scalable Laravel API typically follows a layered architecture:

  1. Routing Layerroutes/api.php with version prefixes and middleware groups.
  2. Controller Layer – Thin controllers that validate input (FormRequest), call services, and return resources.
  3. Service Layer – Business logic, external API calls, and domain events.
  4. Repository / Data Layer – Eloquent models, query scopes, and database transactions.
  5. Presentation Layer – API Resources, Fractal transformers, or custom serializers.

Cross‑cutting concerns (authentication, rate limiting, logging, CORS) live in middleware. Laravel Octane (Swoole/RoadRunner) can keep the application bootstrapped in memory for sub‑millisecond response times.

Step‑by‑Step Guide

1. Scaffold a Fresh Project

composer create-project laravel/laravel api-demo "11.*"
cd api-demo
php artisan --version
# Laravel Framework 11.x.x

2. Configure Environment

Edit .env for database, cache, and queue drivers. For high throughput use Redis for cache/session and a dedicated queue worker.

APP_ENV=production
APP_DEBUG=false
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_DATABASE=api_demo
DB_USERNAME=api_user
DB_PASSWORD=secret
CACHE_DRIVER=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis

3. Install First‑Party Packages

composer require laravel/sanctum laravel/octane
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate
php artisan octane:install --server=swoole

4. Define API Versioning Middleware

// app/Http/Middleware/ApiVersion.php
namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class ApiVersion
{
    public function handle(Request $request, Closure $next, string $version)
    {
        $request->headers->set('Accept', "application/vnd.myapp.v{$version}+json");
        return $next($request);
    }
}

Register in app/Http/Kernel.php under $middlewareAliases as 'api.version' => \App\Http\Middleware\ApiVersion::class.

5. Create Versioned Route Files

// routes/api/v1.php
use Illuminate\Support\Facades\Route;

Route::middleware(['api.version:1', 'auth:sanctum'])->group(function () {
    Route::apiResource('users', UserController::class);
    Route::apiResource('posts', PostController::class);
});

Load them in routes/api.php:

require __DIR__.'/api/v1.php';
// Future versions: require __DIR__.'/api/v2.php';

6. Build FormRequest for Validation

// app/Http/Requests/StoreUserRequest.php
namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;

class StoreUserRequest extends FormRequest
{
    public function authorize(): bool { return true; }

    public function rules(): array
    {
        return [
            'name' => ['required', 'string', 'max:255'],
            'email' => ['required', 'email', 'max:255', Rule::unique('users')],
            'password' => ['required', 'string', 'min:8', 'confirmed'],
        ];
    }
}

7. Implement Service Class

// app/Services/UserService.php
namespace App\Services;

use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Pagination\LengthAwarePaginator;

class UserService
{
    public function list(int $perPage = 15): LengthAwarePaginator
    {
        return User::query()->latest()->paginate($perPage);
    }

    public function create(array $data): User
    {
        return User::create([
            'name' => $data['name'],
            'email' => $data['email'],
            'password' => Hash::make($data['password']),
        ]);
    }

    public function update(User $user, array $data): User
    {
        $user->update($data);
        return $user->fresh();
    }

    public function delete(User $user): void
    {
        $user->delete();
    }
}

8. Create API Resource

// app/Http/Resources/UserResource.php
namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

class UserResource extends JsonResource
{
    public function toArray($request): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'email' => $this->email,
            'created_at' => $this->created_at?->toIso8601String(),
            'updated_at' => $this->updated_at?->toIso8601String(),
            'links' => [
                'self' => route('users.show', $this->id),
                'posts' => route('users.posts.index', $this->id),
            ],
        ];
    }
}

9. Wire Controller

// app/Http/Controllers/Api/V1/UserController.php
namespace App\Http\Controllers\Api\V1;

use App\Http\Controllers\Controller;
use App\Http\Requests\StoreUserRequest;
use App\Http\Requests\UpdateUserRequest;
use App\Http\Resources\UserResource;
use App\Http\Resources\UserCollection;
use App\Services\UserService;
use Illuminate\Http\JsonResponse;

class UserController extends Controller
{
    public function __construct(protected UserService $service) {}

    public function index(): UserCollection
    {
        return new UserCollection($this->service->list(request()->integer('per_page', 15)));
    }

    public function store(StoreUserRequest $request): JsonResponse
    {
        $user = $this->service->create($request->validated());
        return (new UserResource($user))->response()->setStatusCode(201);
    }

    public function show(User $user): UserResource
    {
        return new UserResource($user);
    }

    public function update(UpdateUserRequest $request, User $user): UserResource
    {
        $updated = $this->service->update($user, $request->validated());
        return new UserResource($updated);
    }

    public function destroy(User $user): JsonResponse
    {
        $this->service->delete($user);
        return response()->json(null, 204);
    }
}

10. Generate OpenAPI Documentation

Install darkaonline/l5-swagger and annotate controllers with #[OA\Get] attributes (PHP 8.3 attribute syntax). Run php artisan l5-swagger:generate to produce storage/api-docs/api-docs.json served at /api/documentation.

Real‑World Examples

E‑Commerce Product Catalog

An online store exposes /api/v1/products with filtering, sorting, and pagination. The ProductService uses Eloquent scopes for active(), inCategory($id), and priceBetween($min, $max). The ProductResource conditionally includes variants when the request contains ?include=variants.

Multi‑Tenant SaaS Platform

Each tenant gets a subdomain (tenant1.api.example.com). A middleware resolves the tenant from the host, sets the database connection, and scopes all queries via a global scope. Rate limiting is applied per tenant using Redis keys prefixed with the tenant ID.

Real‑Time Notification Feed

Combine Laravel Echo with Pusher or Soketi. The API returns a notifications resource; the frontend subscribes to a private channel user.{id}.notifications. The backend broadcasts NotificationCreated events from the service layer.

Production Code Examples

Rate Limiter with Dynamic Limits

// app/Providers/RouteServiceProvider.php (boot method)
use Illuminate\Cache\RateLimiter;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;

RateLimiter::for('api', function (Request $request) {
    $user = $request->user();
    $limit = $user?->plan === 'premium' ? 1000 : 100;
    return Limit::perMinute($limit)->by($user?->id ?? $request->ip());
});

Custom Exception Handler for Consistent Error Payloads

// app/Exceptions/Handler.php (render method)
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;

public function render($request, Throwable $e)
{
    if ($request->expectsJson()) {
        if ($e instanceof ValidationException) {
            return response()->json([
                'message' => 'Validation failed',
                'errors' => $e->errors(),
            ], 422);
        }
        if ($e instanceof HttpExceptionInterface) {
            return response()->json([
                'message' => $e->getMessage(),
            ], $e->getStatusCode());
        }
        return response()->json([
            'message' => config('app.debug') ? $e->getMessage() : 'Internal server error',
        ], 500);
    }
    return parent::render($request, $e);
}

Database Transaction Wrapper in Service

// app/Services/OrderService.php
public function placeOrder(array $data): Order
{
    return DB::transaction(function () use ($data) {
        $order = Order::create([
            'user_id' => auth()->id(),
            'total' => $data['total'],
            'status' => 'pending',
        ]);
        foreach ($data['items'] as $item) {
            $order->items()->create($item);
        }
        event(new OrderPlaced($order));
        return $order->load('items');
    });
}

Comparison Table

Feature Laravel API Resources Fractal Transformer Manual JSON
Learning Curve Low (built‑in) Medium (extra package) Low (but repetitive)
Conditional Fields Native when(), mergeWhen() Via includes param Manual if checks
Collection Pagination Automatic with ResourceCollection Manual pagination links Manual
Performance Optimized, lazy loading aware Similar, extra abstraction Fastest if hand‑tuned
OpenAPI Integration Annotations on resources Separate annotation layer None
Community Support Core, long‑term Popular but external N/A

Best Practices

  • Keep controllers thin – delegate to services.
  • Use FormRequest for validation and authorization.
  • Return API Resources, never raw Eloquent models.
  • Version APIs in the URL; deprecate with Sunset header.
  • Implement rate limiting per user or IP.
  • Enable HTTP/2 and TLS termination at the load balancer.
  • Cache immutable responses (e.g., GET /api/v1/categories) with ETag/Last‑Modified.
  • Log structured JSON for observability (Laravel Octane + OpenTelemetry).
  • Write feature tests for every endpoint (Pest or PHPUnit).
  • Automate OpenAPI spec generation in CI.

Common Mistakes

  1. Exposing internal IDs – Use UUIDs or opaque identifiers for public APIs.
  2. Skipping pagination – Returning large collections kills memory and latency.
  3. Inconsistent error formats – Define a single error envelope and reuse it.
  4. Ignoring CORS preflight – Configure fruitcake/laravel-cors correctly.
  5. Hardcoding API keys in code – Store secrets in environment variables or Vault.
  6. Not testing failure paths – Validate 4xx/5xx responses in automated tests.
  7. Over‑fetching relationships – Use whenLoaded to avoid N+1 queries.

Performance Tips

  • Run Laravel Octane with Swoole for persistent worker processes.
  • Enable OPcache and JIT in PHP 8.3 (opcache.jit=1255).
  • Use Redis for cache, session, and queue; cluster for HA.
  • Leverage database indexes on foreign keys and frequently filtered columns.
  • Eager load relationships (with('posts.comments')) and use loadMissing.
  • Compress responses with gzip or brotli at the web server.
  • Offload static assets and API docs to a CDN.
  • Profile with Blackfire or Xdebug in staging; monitor with Laravel Telescope.

Security Considerations

  • Enforce HTTPS everywhere; set SESSION_SECURE_COOKIE=true.
  • Use Sanctum token abilities for fine‑grained scopes.
  • Validate and sanitize all input; never trust $request->all().
  • Implement rate limiting and IP banning for abuse detection.
  • Rotate encryption keys (php artisan key:rotate) periodically.
  • Set security headers via middleware: Content‑Security‑Policy, X‑Frame‑Options, Referrer‑Policy.
  • Audit dependencies with composer audit and npm audit.
  • Log authentication failures and trigger alerts on anomalies.

Deployment Notes

Container Image (Dockerfile)

FROM php:8.3-fpm-alpine

RUN apk add --no-cache \
    nginx \
    supervisor \
    linux-headers \
    $PHPIZE_DEPS \
    && pecl install swoole \
    && docker-php-ext-enable swoole opcache

COPY . /var/www/html
WORKDIR /var/www/html

RUN composer install --no-dev --optimize-autoloader \
    && php artisan config:cache \
    && php artisan route:cache \
    && php artisan view:cache

COPY docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
COPY docker/nginx.conf /etc/nginx/http.d/default.conf

EXPOSE 80
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]

Kubernetes Deployment Snippet

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-demo
spec:
  replicas: 3
  selector:
    matchLabels:
      app: api-demo
  template:
    metadata:
      labels:
        app: api-demo
    spec:
      containers:
      - name: php
        image: myregistry/api-demo:latest
        ports:
        - containerPort: 80
        envFrom:
        - secretRef:
            name: api-demo-secrets
        readinessProbe:
          httpGet:
            path: /health
            port: 80
          initialDelaySeconds: 5
          periodSeconds: 10
      - name: scheduler
        image: myregistry/api-demo:latest
        command: ["php", "artisan", "schedule:work"]

CI/CD Pipeline (GitHub Actions)

name: CI
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.3'
          extensions: mbstring, pdo_mysql, redis, swoole
          coverage: xdebug
      - name: Install dependencies
        run: composer install --prefer-dist --no-progress
      - name: Run static analysis
        run: ./vendor/bin/phpstan analyse
      - name: Run tests
        run: ./vendor/bin/pest --parallel
      - name: Build Docker image
        if: github.ref == 'refs/heads/main'
        run: |
          docker build -t myregistry/api-demo:${{ github.sha }} .
          docker push myregistry/api-demo:${{ github.sha }}

Debugging Tips

  • Enable APP_DEBUG=true locally; use php artisan telescope for request introspection.
  • Log SQL queries with DB::listen in a service provider for slow query detection.
  • Use ray() (Spatie Ray) for real‑time variable inspection without breaking the flow.
  • Inspect Octane worker status via php artisan octane:status.
  • Reproduce production issues in a staging environment that mirrors the exact container image.
  • Correlate logs with trace IDs (OpenTelemetry) across services.

FAQ

What is the minimum PHP version for Laravel 11?

Laravel 11 requires PHP 8.2 or higher; PHP 8.3 is fully supported and recommended for performance gains.

How do I version my API without duplicating routes?

Use a route group prefix (e.g., prefix('v1')) and load separate route files per version. Controllers can live in namespaced folders like App\Http\Controllers\Api\V1.

Can I use Laravel Sanctum for both SPA and mobile token authentication?

Yes. Sanctum provides cookie‑based auth for first‑party SPAs and API token auth for mobile/third‑party clients. Configure sanctum.php accordingly.

What is the best way to handle API pagination?

Return a ResourceCollection which automatically adds links, meta, and data keys compatible with the JSON:API spec.

How do I generate OpenAPI docs automatically?

Install darkaonline/l5-swagger, annotate controllers with PHP 8.3 attributes, and run php artisan l5-swagger:generate in your CI pipeline.

Is Laravel Octane required for high‑traffic APIs?

Octane dramatically reduces bootstrap overhead by keeping the app in memory. For >10k RPM it is highly recommended, but a well‑tuned PHP‑FPM setup can also handle moderate loads.

How do I secure file uploads in an API?

Validate MIME type and size in a FormRequest, store files on a private disk (S3 with signed URLs), and serve them via a signed route that checks authorization.

What testing strategy works best for APIs?

Write feature tests that hit real routes, use a testing database (SQLite in memory), and assert JSON structure with assertJsonStructure. Complement with contract tests for the OpenAPI spec.

Conclusion

You now have a complete blueprint for designing, implementing, and operating a scalable REST API with Laravel 11 and PHP 8.3. By following the layered architecture, leveraging API Resources, enforcing versioning, and adopting Octane plus modern observability tooling, you can ship APIs that are fast, secure, and maintainable. Start by scaffolding a new project, apply the step‑by‑step guide, and iterate with automated tests and CI/CD. If you found this guide valuable, share it with your team and explore the related articles on authentication, performance tuning, and Kubernetes deployment linked above.