W dzisiejszym połączonym świecie REST API stanowią kręgosłup nowoczesnych aplikacji webowych. Laravel 12 oferuje potężne narzędzia i konwencje do tworzenia skalowalnych, łatwych w utrzymaniu i bezpiecznych REST API. Ten kompleksowy przewodnik przeprowadzi Cię przez najlepsze praktyki budowania profesjonalnych API przy użyciu najnowszych możliwości Laravela.
📋 Spis treści
- 🚀 Budowanie solidnych REST API w Laravel 12: kompletny przewodnik
- 📋 Spis treści
- Dlaczego Laravel do REST API?
- Struktura projektu i organizacja
- Strategia wersjonowania API
- Uwierzytelnianie z Laravel Sanctum
- Zasoby API i transformacja danych
- Walidacja żądań i Form Requests
- Zaawansowane filtrowanie ze Spatie Query Builder
- Limitowanie zapytań i bezpieczeństwo
- Testowanie API
- Zakończenie
Dlaczego Laravel do REST API?
Laravel 12 oferuje wyjątkowe funkcje przydatne w tworzeniu API:
- Wbudowane API Resources: spójna transformacja danych w całym API
- Laravel Sanctum: lekkie uwierzytelnianie dla SPA i aplikacji mobilnych
- Form Request Validation: czysta, wielokrotnego użytku logika walidacji
- Wersjonowanie API: wbudowane wsparcie dla wersjonowania API
- Eloquent ORM: potężne operacje na bazie z relacjami
- Stos middleware: elastyczne przetwarzanie żądań i odpowiedzi
- Framework testów: kompleksowe narzędzia testowe w pakiecie
Struktura projektu i organizacja
Dobrze zorganizowana struktura projektu jest kluczowa dla utrzymywalnego API. Oto rekomendowany układ dla projektu API w Laravelu:
app/
├── Http/
│ ├── Controllers/
│ │ └── Api/
│ │ ├── ApiController.php # Bazowy kontroler API
│ │ └── v1/ # Kontrolery specyficzne dla wersji
│ │ ├── ProductController.php
│ │ ├── Auth/
│ │ │ ├── CurrentUserController.php
│ │ │ └── SocialiteLoginController.php
│ │ └── Admin/
│ │ └── ProductController.php
│ ├── Resources/ # Transformatory zasobów API
│ │ ├── ProductResource.php
│ │ └── UserResource.php
│ ├── Requests/ # Walidacja Form Request
│ │ ├── ProductStoreRequest.php
│ │ └── ProductUpdateRequest.php
│ └── Middleware/ # Własne middleware
├── Traits/
│ └── ApiResponses.php # Ustandaryzowane odpowiedzi API
└── Models/
├── Product.php
└── User.php
Strategia wersjonowania API
Wersjonowanie API jest niezbędne do utrzymania kompatybilności wstecznej podczas rozwoju API. Laravel 12 ułatwia to dzięki konfiguracji apiPrefix.
Konfiguracja bootstrap
// bootstrap/app.php
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__ . '/../routes/web.php',
api: __DIR__ . '/../routes/api.php',
commands: __DIR__ . '/../routes/console.php',
channels: __DIR__ . '/../routes/channels.php',
health: '/up',
apiPrefix: 'api/v1',
)
->withMiddleware(function (Middleware $middleware): void {
$middleware->statefulApi();
$middleware->api(prepend: [
ThrottleRequests::class . ':api',
SubstituteBindings::class,
]);
$middleware->alias([
'throttle' => ThrottleRequests::class,
'throttle.login' => ThrottleRequests::class,
'verified' => App\Http\Middleware\EnsureEmailIsVerified::class,
'admin' => App\Http\Middleware\AdminMiddleware::class,
]);
->create();
Organizacja tras
// routes/api.php
Route::prefix('v1')->group(function () {
// Trasy publiczne
Route::get('/products', [ProductController::class, 'index']);
Route::get('/products/{product}', [ProductController::class, 'show']);
// Trasy wymagające uwierzytelnienia
Route::middleware(['auth:sanctum'])->group(function () {
// Trasy administratorskie
Route::middleware(['admin'])->prefix('admin')->group(function () {
Route::apiResource('/products', Admin\ProductController::class);
});
});
});
Uwierzytelnianie z Laravel Sanctum
Laravel Sanctum dostarcza prosty, lekki system uwierzytelniania dla SPA i aplikacji mobilnych.
Konfiguracja
// config/sanctum.php
return [
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
'%s%s',
'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
Sanctum::currentApplicationUrlWithPort()
))),
'guard' => ['web'],
'expiration' => null, // Tokeny nie wygasają
'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''),
'middleware' => [
'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class,
'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class,
'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class,
],
];
Konfiguracja modelu użytkownika
// app/Domain/User/Models/User.php
<?php
namespace App\Domain\User\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens, Notifiable;
protected $fillable = [
'name',
'email',
'password',
];
protected $hidden = [
'password',
'remember_token',
];
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
}
}
Trasy uwierzytelniania
// routes/api.php
Route::get('/sanctum/csrf-cookie', [CsrfCookieController::class, 'show']);
Route::get('/user', fn (Request $request) => $request->user())->middleware('auth:sanctum');
// Endpointy uwierzytelniania
Route::post('/login', [AuthController::class, 'login']);
Route::post('/logout', [AuthController::class, 'logout'])->middleware('auth:sanctum');
Route::post('/register', [AuthController::class, 'register']);
// Uwierzytelnianie społecznościowe
Route::prefix('auth/{provider}')->group(function () {
Route::get('/url', [SocialiteLoginController::class, 'redirectToProvider']);
Route::get('/callback', [SocialiteLoginController::class, 'handleProviderCallback']);
});
Zasoby API i transformacja danych
Zasoby API zapewniają czysty i spójny sposób transformowania modeli do odpowiedzi JSON.
Bazowy kontroler API
// app/Http/Controllers/Api/ApiController.php
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Traits\ApiResponses;
class ApiController extends Controller
{
use ApiResponses;
Ustandaryzowany trait odpowiedzi
// app/Traits/ApiResponses.php
<?php
declare(strict_types=1);
namespace App\Traits;
use Illuminate\Http\JsonResponse;
trait ApiResponses
{
protected function ok($message, $data = [], $statusCode = 200): JsonResponse
{
return $this->success($message, $data, $statusCode);
}
protected function success($message, $data = [], $statusCode = 200): JsonResponse
{
return response()->json([
'data' => $data,
'message' => $message,
'status' => $statusCode,
], $statusCode);
}
protected function error($errors = [], $statusCode = null): JsonResponse
{
if (is_string($errors)) {
return response()->json([
'message' => $errors,
'status' => $statusCode,
], $statusCode);
}
return response()->json([
'errors' => $errors,
]);
}
}
Przykład zasobu API
// app/Http/Resources/ProductResource.php
<?php
declare(strict_types=1);
namespace App\Http\Resources;
use App\Models\Product;
use App\Services\ProductService;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
/** @mixin Product */
class ProductResource extends JsonResource
{
/**
* Przekształć zasób do tablicy.
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'brand_id' => $this->brand_id,
'category_id' => $this->category_id,
'collection_id' => $this->collection_id,
// Warunkowe ładowanie relacji
'brand' => $this->whenLoaded('brand', fn () => $this->brand),
'category' => $this->whenLoaded('category', fn () => $this->category),
'collection' => $this->whenLoaded('collection', fn () => $this->collection),
// Szczegóły produktu
'name' => $this->name,
'model' => $this->model,
'description' => $this->description,
'short_description' => $this->short_description,
// Ceny
'price' => $this->price,
'formatted_price' => $this->whenHas('price', fn () => $this->formatted_price),
// Media
'url' => $this->url,
'images' => $this->images,
'main_image' => $this->main_image,
// Relacje z warunkowym ładowaniem
'sizes' => $this->whenLoaded('sizes', fn () => SizeResource::collection($this->sizes)) ?? [],
'attributes' => $this->whenLoaded('attributes', fn () =>
app(ProductService::class)->transformAttributes($this->attributes)
),
// Dane specyficzne dla użytkownika
'bookmark_id' => $this->when($request->user(), function () use ($request) {
return $this->bookmark()
->whereUserId($request->user()->id)
->pluck('id')->first() ?? false;
}),
'review' => $this->when($request->user(), fn () =>
$this->review()->whereUserId($request->user()->id)->first() ?? false
),
// Dane zagregowane
'reviews_count' => $this->whenCounted('reviews', fn () => $this->reviews_count),
'reviews_avg_rating' => $this->hasAttribute('reviews_avg_rating') && $this->reviews_avg_rating
? round((float) $this->reviews_avg_rating, 2)
: 0,
];
}
}
Walidacja żądań i Form Requests
Form Requests w Laravelu zapewniają czysty sposób obsługi logiki walidacji.
Product Store Request
// app/Http/Requests/ProductStoreRequest.php
<?php
declare(strict_types=1);
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class ProductStoreRequest extends FormRequest
{
/**
* Określ, czy użytkownik jest uprawniony do wykonania tego żądania.
*/
public function authorize(): bool
{
return $this->user()?->hasRole('admin') ?? false;
}
/**
* Zwróć reguły walidacji dla żądania.
*/
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'model' => ['required', 'string', 'max:255'],
'description' => ['required', 'string'],
'short_description' => ['required', 'string', 'max:500'],
'price' => ['required', 'numeric', 'min:0'],
'brand_id' => ['required', 'exists:brands,id'],
'category_id' => ['required', 'exists:categories,id'],
'collection_id' => ['nullable', 'exists:collections,id'],
'images' => ['array'],
'images.*' => ['image', 'mimes:jpeg,png,jpg,webp', 'max:2048'],
'attributes' => ['array'],
'attributes.*.attribute_id' => ['required', 'exists:attributes,id'],
'attributes.*.value' => ['required', 'string'],
];
}
/**
* Własne komunikaty błędów walidacji.
*/
public function messages(): array
{
return [
'name.required' => 'Nazwa produktu jest wymagana.',
'price.required' => 'Cena produktu jest wymagana.',
'price.min' => 'Cena produktu musi być co najmniej 0.',
'images.*.max' => 'Rozmiar obrazu nie może przekraczać 2MB.',
];
}
/**
* Przygotuj dane do walidacji.
*/
protected function prepareForValidation(): void
{
$this->merge([
'price' => $this->price * 100, // Konwersja na grosze
]);
}
}
Zaawansowane filtrowanie ze Spatie Query Builder
Pakiet Spatie Query Builder zapewnia potężne możliwości filtrowania, sortowania i wyszukiwania w Twoim API.
Instalacja
composer require spatie/laravel-query-builder
Podstawowa implementacja
// app/Http/Controllers/Api/v1/ProductController.php
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api\v1;
use App\Http\Controllers\Api\ApiController;
use App\Http\Resources\ProductResource;
use App\Models\Product;
use Illuminate\Http\Request;
use Spatie\QueryBuilder\QueryBuilder;
use Spatie\QueryBuilder\AllowedFilter;
use Spatie\QueryBuilder\AllowedSort;
class ProductController extends ApiController
{
public function index(Request $request)
{
$products = QueryBuilder::for(Product::class)
->allowedFilters([
AllowedFilter::exact('brand_id'),
AllowedFilter::exact('category_id'),
AllowedFilter::exact('collection_id'),
AllowedFilter::partial('name'),
AllowedFilter::scope('price_range'),
AllowedFilter::scope('in_stock'),
])
->allowedSorts([
'name',
'price',
'created_at',
AllowedSort::field('popularity', 'views_count'),
])
->allowedIncludes([
'brand',
'category',
'collection',
'reviews',
'attributes',
])
->defaultSort('-created_at')
->paginate($request->get('per_page', 15));
return ProductResource::collection($products);
}
public function show(Product $product)
{
$product->load([
'brand',
'category',
'collection',
'attributes.attribute',
'reviews.user',
]);
return new ProductResource($product);
}
}
Własne filtry
// app/Models/Product.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Spatie\QueryBuilder\AllowedFilter;
class Product extends Model
{
protected $fillable = [
'name',
'model',
'description',
'short_description',
'price',
'brand_id',
'category_id',
'collection_id',
];
protected $casts = [
'price' => 'integer',
'images' => 'array',
];
public function brand(): BelongsTo
{
return $this->belongsTo(Brand::class);
}
public function category(): BelongsTo
{
return $this->belongsTo(Category::class);
}
public function attributes(): HasMany
{
return $this->hasMany(ProductAttribute::class);
}
/**
* Zakres do filtrowania po przedziale cenowym
*/
public function scopePriceRange($query, $min, $max)
{
return $query->whereBetween('price', [$min * 100, $max * 100]);
}
}
Przykłady użycia
# Podstawowe filtrowanie
GET /api/v1/products?filter[brand_id]=1&filter[category_id]=2
# Wyszukiwanie tekstowe
GET /api/v1/products?filter[name]=laptop
# Filtrowanie po zakresie cenowym
GET /api/v1/products?filter[price_range]=100,500
# Sortowanie
GET /api/v1/products?sort=price&sort=-created_at
# Dołączanie relacji
GET /api/v1/products?include=brand,category,reviews
# Łączenie filtrów
GET /api/v1/products?filter[brand_id]=1&filter[price_range]=100,500&sort=-price&include=brand
Limitowanie zapytań i bezpieczeństwo
Własne ograniczanie liczby zapytań
// routes/api.php
Route::middleware(['throttle:login'])->group(function () {
Route::post('/login', [AuthController::class, 'login']);
Route::post('/register', [AuthController::class, 'register']);
});
Route::middleware(['auth:sanctum', 'throttle:60,1'])->group(function () {
Route::apiResource('products', ProductController::class);
});
Testowanie API
Kompleksowe testy zapewniają poprawne działanie API i jego stabilność w czasie.
Przykład testu funkcjonalnego
// tests/Feature/ProductApiTest.php
<?php
namespace Tests\Feature;
use App\Models\Product;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Laravel\Sanctum\Sanctum;
use Tests\TestCase;
class ProductApiTest extends TestCase
{
use RefreshDatabase;
public function test_can_list_products()
{
Product::factory()->count(3)->create();
$response = $this->getJson('/api/v1/products');
$response->assertStatus(200)
->assertJsonStructure([
'data' => [
'*' => [
'id',
'name',
'price',
'brand',
'category',
]
]
]);
}
public function test_can_filter_products_by_brand()
{
$brand1 = Brand::factory()->create();
$brand2 = Brand::factory()->create();
Product::factory()->create(['brand_id' => $brand1->id]);
Product::factory()->create(['brand_id' => $brand2->id]);
$response = $this->getJson("/api/v1/products?filter[brand_id]={$brand1->id}");
$response->assertStatus(200);
$response->assertJsonCount(1, 'data');
}
public function test_authenticated_user_can_create_product()
{
$user = User::factory()->create();
$user->assignRole('admin');
Sanctum::actingAs($user);
$productData = [
'name' => 'Test Product',
'model' => 'TP-001',
'description' => 'Test description',
'short_description' => 'Short description',
'price' => 99.99,
'brand_id' => Brand::factory()->create()->id,
'category_id' => Category::factory()->create()->id,
];
$response = $this->postJson('/api/v1/products', $productData);
$response->assertStatus(201)
->assertJsonFragment(['name' => 'Test Product']);
$this->assertDatabaseHas('products', [
'name' => 'Test Product',
'price' => 9999, // Cena w groszach
]);
}
public function test_unauthenticated_user_cannot_create_product()
{
$productData = [
'name' => 'Test Product',
'model' => 'TP-001',
'description' => 'Test description',
'price' => 99.99,
];
$response = $this->postJson('/api/v1/products', $productData);
$response->assertStatus(401);
}
}
Zakończenie
Budowanie solidnych REST API w Laravel 12 wymaga dbałości o szczegóły, odpowiedniej architektury i trzymania się najlepszych praktyk. Wdrażając strategie opisane w tym przewodniku, stworzysz API, które są:
- Skalowalne: odpowiednie wersjonowanie i organizacja wspierają rozwój
- Bezpieczne: uwierzytelnianie, walidacja i limitowanie zapytań chronią Twoje API
- Utrzymywalne: czysta struktura kodu i kompleksowe testy
- Przyjazne dla użytkownika: spójne odpowiedzi i klarowna obsługa błędów
- Wydajne: efektywne zapytania i odpowiednie strategie cache
Pamiętaj, aby:
- Zawsze walidować dane wejściowe
- Używać API Resources do spójnej transformacji danych
- Wdrożyć właściwe uwierzytelnianie z Laravel Sanctum
- Wersjonować API od samego początku
- Pisać kompleksowe testy
- Dokumentować endpointy API
- Monitorować wydajność i użycie
Połączenie możliwości Laravela i tych najlepszych praktyk pomoże Ci budować profesjonalne REST API, które będą niezawodne i efektywne.
Obserwuj mnie na LinkedIn po więcej wskazówek o Laravelu!
Chcesz dowiedzieć się więcej o Laravelu, tworzeniu API lub konkretnych implementacjach? Daj znać w komentarzach poniżej!