Skip to main content
Laravel, shipping fast.

Chapter 10

API Evolution

Julian Beaujardin

APIs don't stay the same. Requirements change. You learn better ways to design things. Your users need new features.

Your API will change, so the priority is handling change without breaking users.

Plan for evolution from day one. Use versioning. Deprecate carefully. Communicate changes clearly.

This chapter is about growing your API without turning it into a mess.

Versioning Strategy

/api/v1/domains       ← Initial release
/api/v2/domains       ← Breaking change (new response format)
/api/v3/domains       ← Major refactor

Deprecation Notice

Warn clients before breaking changes:

// V1 endpoint being phased out
class DeprecationHeadersMiddleware
{
    public function handle(Request $request, Closure $next): Response
    {
        // For API v1
        if ($request->is('api/v1/*')) {
            $response = $next($request);

            $response->header('Deprecation', 'true');
            $response->header('Sunset', '2024-12-31T23:59:59Z');  // When v1 dies
            $response->header('Link', '</api/v2/docs>; rel="successor-version"');

            return $response;
        }

        return $next($request);
    }
}

Schema Evolution

Add new fields without breaking existing clients:

// v1 response
{
  "key": "lic_123",
  "name": "License",
  "domain": "example.com"
}

// v2 response (backward compatible)
{
  "key": "lic_123",
  "name": "License",
  "domain": "example.com",
  "status": "active",          // NEW
  "expiresAt": "2025-12-31"   // NEW
}

// v3 response (with additional metadata)
{
  "key": "lic_123",
  "name": "License",
  "domain": "example.com",
  "status": "active",
  "expiresAt": "2025-12-31",
  "metadata": {               // NEW
    "createdAt": "2024-01-01",
    "lastModified": "2024-12-01"
  }
}
  "expires_at": "2025-01-05"
}

Handling Multiple Versions

// routes/api.php
Route::prefix('v1')->group(function () {
    Route::get('/domains', LicenseControllerV1::class);
});

Route::prefix('v2')->group(function () {
    Route::get('/domains', LicenseControllerV2::class);
});

// Or use content negotiation
Route::get('/domains', function (Request $request) {
    $version = $request->header('API-Version', 'v1');

    return match($version) {
        'v1' => new CollectionResponse(LicenseResourceV1::collection(...)),
        'v2' => new CollectionResponse(LicenseResourceV2::collection(...)),
        default => response()->json(['error' => 'Unsupported version'], 400),
    };
});

Changelog Tracking

Keep a detailed changelog:

# Changelog

# [2.0.0] - 2024-12-01

# Added
- New `/api/v2/domains` endpoint with improved response format
- Support for bulk domain operations
- WebSocket support for real-time updates

# Changed
- Response format redesigned for clarity
- Rate limit headers now more granular

# Deprecated
- `/api/v1/domains` - use `/api/v2/domains` instead
- Bearer token format changed - old format still supported until 2024-03-01

# Removed
- Legacy SOAP API endpoint

# Security
- Fixed authentication bypass vulnerability

# [1.2.1] - 2024-10-15

# Fixed
- Rate limiting incorrectly counted requests from different IPs

Chapter 10 Summary

Versioning is essential. Plan for change from day one.

Best practices:

  1. Use URL versioning: /api/v1, /api/v2
  2. Deprecate old versions with warning period
  3. Support 2-3 versions simultaneously
  4. Publish deprecation headers and dates
  5. Changelog every change
  6. Make migration guides for breaking changes

Evolution is inevitable. Plan for it.