API evolution is inevitable. As your application grows, you'll need to phase out old endpoints while maintaining backward compatibility. But what happens when you need to deprecate only one HTTP method of an endpoint while keeping others active?
In this post, I'll show you how to handle a common scenario: deprecating a GET request while keeping the POST request active on the same route, using the same controller.
The Problem
Imagine you have this route setup:
// modules/Products/Routes/api.php
Route::get('/products/{id}/export', ProductExportController::class)->name('export');
Route::post('/products/{id}/export', ProductExportController::class)->name('export');
Both routes use the same controller, but you want to deprecate the GET method while keeping POST active. Simply adding deprecation headers in the controller would affect both methods.
Why Proper Deprecation Matters
Before diving into solutions, let's understand why this matters:
- Client Communication: Consumers need advance notice to migrate
- Standards Compliance: HTTP has standard headers for deprecation (Deprecation, Sunset)
- Monitoring: Track which clients still use deprecated endpoints
- Graceful Migration: Avoid breaking production systems
Solution 1: Separate Legacy Controller (Recommended)
The cleanest approach is to create a dedicated controller for the deprecated endpoint that delegates to the main controller while adding deprecation headers.
Step 1: Create the Legacy Controller
// app/Http/Controllers/ProductExportLegacyController.php
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class ProductExportLegacyController
{
public function __invoke(Request $request, $id)
{
// Delegate to the main controller
$response = app(ProductExportController::class)($request, $id);
// Add deprecation headers
return $response
->header('Deprecation', 'true')
->header('Sunset', 'Sat, 01 Jun 2026 00:00:00 GMT')
->header('X-API-Warn', 'This endpoint is deprecated. Use POST /products/{id}/export instead');
}
}
Step 2: Update Your Routes
// Deprecated GET endpoint
Route::get('/products/{id}/export', ProductExportLegacyController::class)
->name('exportLegacy'); // DEPRECATED - Use POST instead
// Active POST endpoint
Route::post('/products/{id}/export', ProductExportController::class)
->name('export');
Advantages
- ✅ Clear separation of concerns
- ✅ Easy to remove when deprecation period ends
- ✅ No conditional logic in main controller
- ✅ Self-documenting code
Solution 2: Method-Aware Middleware
If you prefer middleware, create one that checks the HTTP method before adding headers.
Step 1: Create the Middleware
php artisan make:middleware DeprecateGetMethod
// app/Http/Middleware/DeprecateGetMethod.php
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class DeprecateGetMethod
{
public function handle(Request $request, Closure $next, ?string $sunset = null, ?string $message = null)
{
$response = $next($request);
if ($request->isMethod('GET')) {
$response->headers->set('Deprecation', 'true');
$response->headers->set('Sunset', $sunset ?? now()->addMonths(6)->toRfc7231String());
if ($message) {
$response->headers->set('X-API-Warn', $message);
}
}
return $response;
}
}
Step 2: Register the Middleware
// bootstrap/app.php or app/Http/Kernel.php
protected $middlewareAliases = [
// ... other middleware
'deprecate.get' => \App\Http\Middleware\DeprecateGetMethod::class,
];
Step 3: Apply to Route
Route::get('/products/{id}/export', ProductExportController::class)
->name('exportLegacy')
->middleware('deprecate.get:2026-06-01,Use POST method instead');
Route::post('/products/{id}/export', ProductExportController::class)
->name('export');
Advantages
- ✅ Reusable across multiple routes
- ✅ Parameterized sunset date and message
- ✅ Clean route definitions
Solution 3: Generic Deprecation Middleware
For maximum flexibility, create a middleware that works with any HTTP method.
// app/Http/Middleware/DeprecateEndpoint.php
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class DeprecateEndpoint
{
public function handle(Request $request, Closure $next, ?string $methods = null, ?string $sunset = null, ?string $replacement = null)
{
$response = $next($request);
// If no methods specified, deprecate all
// If methods specified, only deprecate those methods
$deprecatedMethods = $methods ? explode(',', strtoupper($methods)) : ['*'];
if ($deprecatedMethods === ['*'] || in_array($request->method(), $deprecatedMethods)) {
$response->headers->set('Deprecation', 'true');
$response->headers->set('Sunset', $sunset ?? now()->addMonths(6)->toRfc7231String());
if ($replacement) {
$response->headers->set('Link', "<{$replacement}>; rel=\"alternate\"");
}
}
return $response;
}
}
Usage:
// Deprecate only GET and DELETE methods
Route::match(['get', 'delete'], '/products/{id}/export', ProductExportController::class)
->middleware('deprecated:GET|DELETE,2026-06-01,/api/v2/products/{id}/export');
Understanding Deprecation Headers
Let's break down the HTTP headers we're using:
Deprecation: true
- Standard: https://datatracker.ietf.org/doc/draft-ietf-httpapi-deprecation-header/09/
- Purpose: Indicates the endpoint is deprecated
- Client Action: Log warnings, alert developers
Sunset:
- Standard: https://datatracker.ietf.org/doc/html/rfc8594
- Format: RFC 7231 date (e.g., Sat, 01 Jun 2026 00:00:00 GMT)
- Purpose: When the endpoint will be removed
- Client Action: Plan migration before this date
X-API-Warn:
- Standard: Custom (common practice)
- Purpose: Human-readable deprecation message
- Client Action: Display to developers
Link: ; rel="alternate"
- Standard: https://datatracker.ietf.org/doc/html/rfc8288
- Purpose: Points to replacement endpoint
- Client Action: Use this URL instead
Best Practices
1. Document Everywhere
/**
* @deprecated This GET endpoint is obsolete as of v2.5.0
* Will be removed in v3.0.0 (June 2026)
* Use POST /products/{id}/export instead
*/
Route::get('/products/{id}/export', ProductExportLegacyController::class)
->name('exportLegacy');
2. Log Deprecated Usage
Track who's still using deprecated endpoints:
public function __invoke(Request $request, $id)
{
Log::warning('Deprecated GET /products/export endpoint used', [
'product_id' => $id,
'client_ip' => $request->ip(),
'user_agent' => $request->userAgent(),
]);
// ... rest of controller
}
3. Communicate the Change
- Update API documentation
- Send email notifications to API consumers
- Add changelog entries
- Update OpenAPI/Swagger specs
4. Set Realistic Sunset Dates
Give clients enough time to migrate:
- Minor changes: 3-6 months
- Major changes: 6-12 months
- Breaking changes: 12+ months
5. Version Your API
Consider versioned routes for major changes:
// Old version (deprecated)
Route::prefix('v1')->group(function () {
Route::get('/products/{id}/export', ProductExportLegacyController::class);
});
// New version
Route::prefix('v2')->group(function () {
Route::post('/products/{id}/export', ProductExportController::class);
});
Monitoring Deprecated Endpoints
Create a simple monitoring system:
// app/Http/Middleware/TrackDeprecatedUsage.php
public function handle(Request $request, Closure $next)
{
$response = $next($request);
if ($response->headers->has('Deprecation')) {
event(new DeprecatedEndpointAccessed(
endpoint: $request->path(),
method: $request->method(),
user: $request->user(),
ip: $request->ip(),
));
}
return $response;
}
Then create a dashboard or report:
// Track usage over time
DB::table('deprecated_endpoint_usage')
->select('endpoint', DB::raw('count(*) as hits'))
->where('accessed_at', '>', now()->subDays(30))
->groupBy('endpoint')
->get();
The Removal Process
When the sunset date arrives:
1. Final Warning Period (1-2 weeks before)
public function __invoke(Request $request, $id)
{
Log::critical('FINAL WARNING: Deprecated endpoint will be removed in 2 weeks', [
'endpoint' => $request->path(),
'removal_date' => '2026-06-01',
]);
// ... rest of controller
}
2. Graceful Removal
// Return 410 Gone instead of 404 Not Found
Route::get('/products/{id}/export', function () {
return response()->json([
'message' => 'This endpoint was removed on June 1, 2026',
'replacement' => 'POST /products/{id}/export',
'documentation' => 'https://docs.example.com/api/v2/products',
], 410);
})->name('exportRemoved');
3. Complete Removal
After a grace period (e.g., 3-6 months), remove the route entirely.
Conclusion
Deprecating specific HTTP methods on shared endpoints requires thoughtful implementation. The separate controller approach offers the best balance of clarity and maintainability, while middleware solutions provide reusability across your API.
Remember:
✅ Use standard HTTP headers
✅ Provide clear migration paths
✅ Give adequate notice
✅ Monitor usage
✅ Document everything
Your API consumers will thank you for the smooth transition!
Top comments (0)