Niezawodne API jest kontraktem na granicy aplikacji. Klient potrzebuje stabilnych nazw, przewidywalnej paginacji, użytecznych błędów i bezpiecznych ponowień. Laravel dostarcza elementy, ale jakość produkcyjna wynika ze świadomej polityki kompatybilności, autoryzacji i awarii.
Zacznij od polityki kompatybilności
Wersjonuj publiczne API, gdy niezależnie wdrażani klienci potrzebują jasnej obietnicy. /api/v1/products łatwo udokumentować i wycofać; nie jest rytuałem dla prywatnego backendu Inertia wdrażanego razem z serwerem. Po publikacji v1 nie zmieniaj po cichu znaczenia pola: wystaw v2 obok i określ okres migracji.
<?php
declare(strict_types=1);
use App\Http\Controllers\Api\V1\ProductController;
use Illuminate\Support\Facades\Route;
Route::prefix('v1')->middleware('auth:sanctum')->group(function (): void {
Route::apiResource('products', ProductController::class);
});
apiResource daje przewidywalną powierzchnię. Route model binding odnajduje model, a policy rozstrzyga dostęp; nie duplikuj kontroli własności w kontrolerach.
Rozdziel wejście, przypadek użycia i reprezentację
Form Request waliduje wejście HTTP, akcja wykonuje przypadek użycia, a Resource posiada publiczną reprezentację. Dzięki temu przypadkowa kolumna modelu nie staje się częścią API na zawsze.
<?php
declare(strict_types=1);
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
final class ProductResource extends JsonResource
{
/** @return array<string, int|string> */
public function toArray(Request $request): array
{
return ['id' => $this->resource->getKey(), 'name' => $this->resource->name, 'price_cents' => $this->resource->price_cents];
}
}
Zwracaj ProductResource::collection($products), nie kolekcję Eloquent. Resource jest jednym miejscem na linki, zmienione nazwy pól i warunkowe relacje bez wycieku wnętrza aplikacji.
Nadaj tokenom wąskie abilities
Tokeny Sanctum są poświadczeniami, nie rolami. Wydaj tylko abilities potrzebne integracji i wymuś je middleware. Token raportowania nie powinien usuwać produktów.
<?php
declare(strict_types=1);
$token = $user->createToken('warehouse-sync', ['products:read']);
Route::get('/v1/products', ProductController::class)
->middleware(['auth:sanctum', 'abilities:products:read']);
Policy odpowiada za dostęp użytkownika, abilities za zakres konkretnego tokenu. Rotuj i unieważniaj tokeny, zapisuj ich cel i nigdy nie umieszczaj długowiecznego tokenu w JavaScripcie przeglądarki.
Dobierz paginację do zapytania
Offset (paginate()) daje total i numery stron, ale inserty przesuwają elementy między stronami, a duży offset jest kosztowny. Cursor (cursorPaginate()) szuka po stabilnym, zindeksowanym porządku, dlatego pasuje do feedów i dużych tabel dopisywanych w czasie.
<?php
declare(strict_types=1);
$products = Product::query()->orderBy('id')->cursorPaginate(perPage: 50);
return ProductResource::collection($products);
Kolejność kursora musi być deterministyczna. Użyj indeksowanego, unikalnego rozstrzygacza, np. created_at, id; nie paginuj kursorem po niestabilnej wartości obliczanej. Offset wybierz, gdy człowiek faktycznie potrzebuje strony 42.
Uczyń błędy czytelnymi dla maszyny
RFC 9457 Problem Details daje walidacji, autoryzacji i awariom jeden rozpoznawalny kształt. Zwracaj application/problem+json, stabilne type, krótkie title, HTTP status, instance albo ID korelacyjne oraz błędy pól tylko przy walidacji. Mapowanie centralizuj w konfiguracji wyjątków Laravela; kontrolery powinny zwracać zasoby sukcesu, nie różne tablice błędów.
{"type":"https://api.example.com/problems/validation","title":"The request is invalid.","status":422,"errors":{"name":["The name field is required."]}}
Nigdy nie wysyłaj klientowi wiadomości wyjątku, SQL ani stack trace.
Limituj chronioną możliwość
Nazwane limitery trzymaj w providerze, gdzie klucz i polityka są widoczne w review.
<?php
declare(strict_types=1);
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
RateLimiter::for('partner-api', fn (Request $request): Limit => Limit::perMinute(120)->by((string) $request->user()?->getAuthIdentifier()));
Podłącz throttle:partner-api do grupy tras. Dla ruchu anonimowego użyj zaufanej tożsamości; niezaufany nagłówek forwarded-IP nią nie jest. Zwracaj 429 z informacją o ponowieniu i alarmuj o stałym limitowaniu partnera.
Pułapki i kiedy tego NIE używać
Nie dodawaj wersjonowanego REST tylko dlatego, że brzmi nowocześnie: wewnętrzny formularz nie potrzebuje publicznej obietnicy kompatybilności. Nie używaj kursora przy dowolnym sortowaniu bez indeksu i tie-breakera. Nie traktuj abilities Sanctum jako pełnego modelu uprawnień. HTTP retry nie jest exactly-once: płatności i webhooki potrzebują idempotency key oraz unikalnego ograniczenia bazy. Przed publikacją testuj feature każdy publiczny element kontraktu: sukces, oczekiwany błąd, autoryzację, paginację i ponowienie.