Przejdź do treści
Backend

Budowanie REST API w Laravel z Sanctum - kompletny przewodnik

Opublikowano:
·4 min czytania·Autor: MDS Software Solutions Group

Budowanie REST API

backend

Budowanie REST API w Laravel z Sanctum - kompletny przewodnik

Laravel to jeden z najpopularniejszych frameworkow PHP, ktory oferuje eleganckie narzedzia do budowania nowoczesnych REST API. W polaczeniu z Laravel Sanctum - lekkim systemem uwierzytelniania - mozesz stworzyc bezpieczne, wydajne i skalowalne API w krotkim czasie. W tym przewodniku przeprowadzimy Cie przez caly proces: od konfiguracji projektu, przez projektowanie endpointow RESTful, az po uwierzytelnianie, walidacje, testowanie i dokumentacje.

Tworzenie projektu Laravel API#

Rozpocznij od utworzenia nowego projektu Laravel i skonfigurowania go pod API:

# Zainstaluj nowy projekt Laravel
composer create-project laravel/laravel my-api-project
cd my-api-project

# Skonfiguruj plik .env z polaczeniem do bazy danych
# DB_CONNECTION=mysql
# DB_HOST=127.0.0.1
# DB_PORT=3306
# DB_DATABASE=my_api
# DB_USERNAME=root
# DB_PASSWORD=secret

# Uruchom migracje
php artisan migrate

Laravel domyslnie zawiera plik routes/api.php, w ktorym definiujesz trasy API. Wszystkie trasy w tym pliku automatycznie otrzymuja prefiks /api i stosuja middleware api.

Struktura projektu API#

Dobrze zorganizowany projekt API w Laravel powinien miec przejrzysta strukture katalogow:

app/
  Http/
    Controllers/
      Api/
        V1/
          ArticleController.php
          AuthController.php
          CommentController.php
    Requests/
      StoreArticleRequest.php
      UpdateArticleRequest.php
    Resources/
      ArticleResource.php
      ArticleCollection.php
  Models/
    Article.php
    Comment.php
  Policies/
    ArticlePolicy.php
routes/
  api.php
  api_v1.php
tests/
  Feature/
    Api/
      ArticleTest.php
      AuthTest.php

Zasady projektowania RESTful API#

Zanim przejdziemy do implementacji, warto znac kluczowe zasady projektowania REST API:

Konwencje nazewnictwa zasobow#

Uzywaj rzeczownikow w liczbie mnogiej do nazywania zasobow i HTTP verbs do okreslania akcji:

| Metoda HTTP | Endpoint | Opis | |-------------|----------------------|-------------------------| | GET | /api/articles | Lista artykulow | | GET | /api/articles/{id} | Pojedynczy artykul | | POST | /api/articles | Utworz artykul | | PUT/PATCH | /api/articles/{id} | Aktualizuj artykul | | DELETE | /api/articles/{id} | Usun artykul |

Kody odpowiedzi HTTP#

Stosuj poprawne kody statusu HTTP:

  • 200 OK - udane pobranie lub aktualizacja
  • 201 Created - udane utworzenie zasobu
  • 204 No Content - udane usuniecie
  • 400 Bad Request - bledne dane wejsciowe
  • 401 Unauthorized - brak uwierzytelniania
  • 403 Forbidden - brak uprawnien
  • 404 Not Found - zasob nie znaleziony
  • 422 Unprocessable Entity - blad walidacji
  • 429 Too Many Requests - przekroczenie limitu zapytan
  • 500 Internal Server Error - blad serwera

API Resource Controllers#

Laravel pozwala na szybkie tworzenie kontrolerow z pelnym zestawem metod CRUD za pomoca jednej komendy:

php artisan make:controller Api/V1/ArticleController --api --model=Article

Flaga --api generuje kontroler bez metod create i edit (formularze HTML), ktore sa niepotrzebne w API:

<?php

namespace App\Http\Controllers\Api\V1;

use App\Http\Controllers\Controller;
use App\Http\Requests\StoreArticleRequest;
use App\Http\Requests\UpdateArticleRequest;
use App\Http\Resources\ArticleResource;
use App\Http\Resources\ArticleCollection;
use App\Models\Article;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class ArticleController extends Controller
{
    /**
     * Wyswietl liste artykulow z paginacja.
     */
    public function index(Request $request): ArticleCollection
    {
        $articles = Article::query()
            ->with(['author', 'tags'])
            ->when($request->query('search'), function ($query, $search) {
                $query->where('title', 'like', "%{$search}%")
                      ->orWhere('content', 'like', "%{$search}%");
            })
            ->when($request->query('tag'), function ($query, $tag) {
                $query->whereHas('tags', fn ($q) => $q->where('slug', $tag));
            })
            ->when($request->query('sort'), function ($query, $sort) {
                $direction = str_starts_with($sort, '-') ? 'desc' : 'asc';
                $column = ltrim($sort, '-');
                $query->orderBy($column, $direction);
            }, function ($query) {
                $query->latest();
            })
            ->paginate($request->query('per_page', 15));

        return new ArticleCollection($articles);
    }

    /**
     * Zapisz nowy artykul.
     */
    public function store(StoreArticleRequest $request): JsonResponse
    {
        $article = Article::create([
            ...$request->validated(),
            'user_id' => $request->user()->id,
            'slug' => str()->slug($request->title),
        ]);

        $article->tags()->sync($request->tag_ids ?? []);

        return (new ArticleResource($article->load(['author', 'tags'])))
            ->response()
            ->setStatusCode(201);
    }

    /**
     * Wyswietl pojedynczy artykul.
     */
    public function show(Article $article): ArticleResource
    {
        return new ArticleResource(
            $article->load(['author', 'tags', 'comments.user'])
        );
    }

    /**
     * Aktualizuj artykul.
     */
    public function update(UpdateArticleRequest $request, Article $article): ArticleResource
    {
        $article->update($request->validated());

        if ($request->has('tag_ids')) {
            $article->tags()->sync($request->tag_ids);
        }

        return new ArticleResource($article->fresh(['author', 'tags']));
    }

    /**
     * Usun artykul.
     */
    public function destroy(Article $article): JsonResponse
    {
        $this->authorize('delete', $article);

        $article->delete();

        return response()->json(null, 204);
    }
}

Rejestracja tras#

Zarejestruj kontroler w routes/api.php:

use App\Http\Controllers\Api\V1\ArticleController;
use App\Http\Controllers\Api\V1\AuthController;
use Illuminate\Support\Facades\Route;

// Trasy publiczne
Route::prefix('v1')->group(function () {
    Route::post('/register', [AuthController::class, 'register']);
    Route::post('/login', [AuthController::class, 'login']);

    Route::get('/articles', [ArticleController::class, 'index']);
    Route::get('/articles/{article}', [ArticleController::class, 'show']);
});

// Trasy chronione
Route::prefix('v1')->middleware('auth:sanctum')->group(function () {
    Route::post('/logout', [AuthController::class, 'logout']);

    Route::post('/articles', [ArticleController::class, 'store']);
    Route::put('/articles/{article}', [ArticleController::class, 'update']);
    Route::delete('/articles/{article}', [ArticleController::class, 'destroy']);
});

Eloquent API Resources - transformacja danych#

API Resources pozwalaja na kontrolowane transformowanie modeli Eloquent do odpowiedzi JSON. To warstwa abstrakcji miedzy modelami a odpowiedziami API:

php artisan make:resource ArticleResource
php artisan make:resource ArticleCollection --collection

Definiowanie zasobu#

<?php

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class ArticleResource extends JsonResource
{
    /**
     * Transformuj zasob do tablicy.
     */
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'type' => 'article',
            'attributes' => [
                'title' => $this->title,
                'slug' => $this->slug,
                'excerpt' => str()->limit($this->content, 200),
                'content' => $this->when(
                    $request->routeIs('articles.show'),
                    $this->content
                ),
                'reading_time' => ceil(str_word_count($this->content) / 200) . ' min',
                'published_at' => $this->published_at?->toISOString(),
                'created_at' => $this->created_at->toISOString(),
                'updated_at' => $this->updated_at->toISOString(),
            ],
            'relationships' => [
                'author' => [
                    'id' => $this->author->id,
                    'name' => $this->author->name,
                ],
                'tags' => $this->whenLoaded('tags', function () {
                    return $this->tags->map(fn ($tag) => [
                        'id' => $tag->id,
                        'name' => $tag->name,
                        'slug' => $tag->slug,
                    ]);
                }),
                'comments_count' => $this->whenCounted('comments'),
            ],
            'links' => [
                'self' => route('api.v1.articles.show', $this->id),
            ],
        ];
    }
}

Kolekcja zasobow z metadanymi#

<?php

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;

class ArticleCollection extends ResourceCollection
{
    public $collects = ArticleResource::class;

    public function toArray(Request $request): array
    {
        return [
            'data' => $this->collection,
            'meta' => [
                'total' => $this->total(),
                'per_page' => $this->perPage(),
                'current_page' => $this->currentPage(),
                'last_page' => $this->lastPage(),
                'has_more_pages' => $this->hasMorePages(),
            ],
            'links' => [
                'first' => $this->url(1),
                'last' => $this->url($this->lastPage()),
                'prev' => $this->previousPageUrl(),
                'next' => $this->nextPageUrl(),
            ],
        ];
    }
}

Laravel Sanctum - konfiguracja i uwierzytelnianie#

Laravel Sanctum oferuje dwa tryby uwierzytelniania: tokeny API dla aplikacji mobilnych i zewnetrznych klientow oraz uwierzytelnianie oparte na sesjach (cookies) dla SPA.

Instalacja i konfiguracja#

# Sanctum jest dolaczony domyslnie od Laravel 11
# Dla starszych wersji:
composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate

Upewnij sie, ze model User uzywa traitu HasApiTokens:

<?php

namespace App\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',
        ];
    }
}

Kontroler uwierzytelniania z tokenami#

<?php

namespace App\Http\Controllers\Api\V1;

use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;

class AuthController extends Controller
{
    /**
     * Rejestracja nowego uzytkownika.
     */
    public function register(Request $request): JsonResponse
    {
        $validated = $request->validate([
            'name' => ['required', 'string', 'max:255'],
            'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
            'password' => ['required', 'string', 'min:8', 'confirmed'],
        ]);

        $user = User::create([
            'name' => $validated['name'],
            'email' => $validated['email'],
            'password' => Hash::make($validated['password']),
        ]);

        $token = $user->createToken('auth-token', ['*'], now()->addDays(30));

        return response()->json([
            'message' => 'Konto zostalo utworzone pomyslnie.',
            'user' => [
                'id' => $user->id,
                'name' => $user->name,
                'email' => $user->email,
            ],
            'token' => $token->plainTextToken,
            'token_type' => 'Bearer',
            'expires_at' => $token->accessToken->expires_at->toISOString(),
        ], 201);
    }

    /**
     * Logowanie uzytkownika i generowanie tokena.
     */
    public function login(Request $request): JsonResponse
    {
        $request->validate([
            'email' => ['required', 'email'],
            'password' => ['required'],
        ]);

        $user = User::where('email', $request->email)->first();

        if (! $user || ! Hash::check($request->password, $user->password)) {
            throw ValidationException::withMessages([
                'email' => ['Podane dane logowania sa nieprawidlowe.'],
            ]);
        }

        // Opcjonalnie: usun stare tokeny
        // $user->tokens()->delete();

        $token = $user->createToken(
            'auth-token',
            $this->getAbilitiesForUser($user),
            now()->addDays(30)
        );

        return response()->json([
            'message' => 'Zalogowano pomyslnie.',
            'user' => [
                'id' => $user->id,
                'name' => $user->name,
                'email' => $user->email,
            ],
            'token' => $token->plainTextToken,
            'token_type' => 'Bearer',
        ]);
    }

    /**
     * Wylogowanie - uniewaznij biezacy token.
     */
    public function logout(Request $request): JsonResponse
    {
        $request->user()->currentAccessToken()->delete();

        return response()->json([
            'message' => 'Wylogowano pomyslnie.',
        ]);
    }

    /**
     * Pobierz uprawnienia na podstawie roli uzytkownika.
     */
    private function getAbilitiesForUser(User $user): array
    {
        return match ($user->role) {
            'admin' => ['*'],
            'editor' => ['articles:create', 'articles:update', 'articles:delete', 'comments:manage'],
            'author' => ['articles:create', 'articles:update', 'comments:create'],
            default => ['articles:read', 'comments:create'],
        };
    }
}

Sprawdzanie uprawnien tokenow#

// W kontrolerze lub middleware
public function store(StoreArticleRequest $request): JsonResponse
{
    if (! $request->user()->tokenCan('articles:create')) {
        abort(403, 'Brak uprawnien do tworzenia artykulow.');
    }

    // ... logika tworzenia
}

Uwierzytelnianie SPA z Sanctum#

Dla aplikacji Single Page Application (np. React, Vue, Angular) Sanctum oferuje uwierzytelnianie oparte na cookies:

Konfiguracja CORS i Sanctum#

W config/sanctum.php:

'stateful' => explode(',', env(
    'SANCTUM_STATEFUL_DOMAINS',
    'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000'
)),

W config/cors.php:

return [
    'paths' => ['api/*', 'sanctum/csrf-cookie'],
    'allowed_origins' => [env('FRONTEND_URL', 'http://localhost:3000')],
    'supports_credentials' => true,
];

Przeplyw uwierzytelniania SPA#

// 1. Pobierz CSRF cookie
await fetch('/sanctum/csrf-cookie', {
    method: 'GET',
    credentials: 'include',
});

// 2. Zaloguj sie
const response = await fetch('/api/v1/login', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'Accept': 'application/json',
        'X-XSRF-TOKEN': getCookie('XSRF-TOKEN'),
    },
    credentials: 'include',
    body: JSON.stringify({
        email: 'user@example.com',
        password: 'password',
    }),
});

// 3. Kolejne zapytania sa automatycznie uwierzytelniane przez cookies
const articles = await fetch('/api/v1/articles', {
    headers: { 'Accept': 'application/json' },
    credentials: 'include',
});

Rate limiting - ograniczanie liczby zapytan#

Laravel oferuje elastyczny system rate limitingu zdefiniowany w AppServiceProvider lub RouteServiceProvider:

<?php

namespace App\Providers;

use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        // Globalny limit API
        RateLimiter::for('api', function (Request $request) {
            return Limit::perMinute(60)->by(
                $request->user()?->id ?: $request->ip()
            )->response(function () {
                return response()->json([
                    'message' => 'Zbyt wiele zapytan. Sprobuj ponownie pozniej.',
                    'retry_after' => 60,
                ], 429);
            });
        });

        // Restrykcyjny limit dla logowania
        RateLimiter::for('auth', function (Request $request) {
            return [
                Limit::perMinute(5)->by($request->ip()),
                Limit::perMinute(10)->by($request->input('email')),
            ];
        });

        // Wyzszy limit dla uwierzytelnionych uzytkownikow
        RateLimiter::for('authenticated', function (Request $request) {
            return $request->user()?->is_premium
                ? Limit::perMinute(120)->by($request->user()->id)
                : Limit::perMinute(60)->by($request->user()->id);
        });
    }
}

Zastosowanie w trasach:

Route::middleware(['throttle:auth'])->group(function () {
    Route::post('/login', [AuthController::class, 'login']);
    Route::post('/register', [AuthController::class, 'register']);
});

Route::middleware(['auth:sanctum', 'throttle:authenticated'])->group(function () {
    Route::apiResource('articles', ArticleController::class);
});

Wersjonowanie API#

Wersjonowanie pozwala na wprowadzanie zmian bez lamia kompatybilnosci z istniejacymi klientami:

Strategia z prefiksem URL#

// routes/api.php
Route::prefix('v1')->group(function () {
    Route::apiResource('articles', Api\V1\ArticleController::class);
});

Route::prefix('v2')->group(function () {
    Route::apiResource('articles', Api\V2\ArticleController::class);
});

Strategia z naglowkiem#

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

use Closure;
use Illuminate\Http\Request;

class ApiVersionMiddleware
{
    public function handle(Request $request, Closure $next)
    {
        $version = $request->header('X-API-Version', 'v1');

        config(['app.api_version' => $version]);

        return $next($request);
    }
}

Wykorzystanie abstrakcyjnego kontrolera bazowego#

<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;

abstract class BaseApiController extends Controller
{
    protected function successResponse(mixed $data, int $statusCode = 200)
    {
        return response()->json([
            'success' => true,
            'data' => $data,
            'api_version' => config('app.api_version', 'v1'),
        ], $statusCode);
    }

    protected function errorResponse(string $message, int $statusCode, array $errors = [])
    {
        return response()->json([
            'success' => false,
            'message' => $message,
            'errors' => $errors ?: null,
            'api_version' => config('app.api_version', 'v1'),
        ], $statusCode);
    }
}

Walidacja z Form Requests#

Form Requests to elegancki sposob na wydzielenie logiki walidacji z kontrolerow:

php artisan make:request StoreArticleRequest
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Http\Exceptions\HttpResponseException;

class StoreArticleRequest extends FormRequest
{
    public function authorize(): bool
    {
        return $this->user()->tokenCan('articles:create');
    }

    public function rules(): array
    {
        return [
            'title' => ['required', 'string', 'max:255', 'unique:articles,title'],
            'content' => ['required', 'string', 'min:100'],
            'excerpt' => ['nullable', 'string', 'max:500'],
            'category_id' => ['required', 'exists:categories,id'],
            'tag_ids' => ['nullable', 'array'],
            'tag_ids.*' => ['exists:tags,id'],
            'published_at' => ['nullable', 'date', 'after_or_equal:today'],
            'featured_image' => ['nullable', 'url', 'max:2048'],
            'status' => ['sometimes', 'in:draft,published,scheduled'],
        ];
    }

    public function messages(): array
    {
        return [
            'title.required' => 'Tytul artykulu jest wymagany.',
            'title.unique' => 'Artykul o takim tytule juz istnieje.',
            'content.min' => 'Tresc artykulu musi miec co najmniej :min znakow.',
            'category_id.exists' => 'Wybrana kategoria nie istnieje.',
        ];
    }

    /**
     * Zwroc odpowiedz JSON w przypadku bledu walidacji.
     */
    protected function failedValidation(Validator $validator): void
    {
        throw new HttpResponseException(
            response()->json([
                'success' => false,
                'message' => 'Blad walidacji danych.',
                'errors' => $validator->errors(),
            ], 422)
        );
    }
}

Obsluga bledow i odpowiedzi API#

Skonfiguruj globalna obsluge bledow w bootstrap/app.php (Laravel 11+):

<?php

use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Exception\ThrottleRequestsException;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        api: __DIR__.'/../routes/api.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware) {
        $middleware->statefulApi();
    })
    ->withExceptions(function (Exceptions $exceptions) {
        $exceptions->shouldRenderJsonWhen(function ($request) {
            return $request->is('api/*') || $request->expectsJson();
        });

        $exceptions->render(function (AuthenticationException $e) {
            return response()->json([
                'success' => false,
                'message' => 'Wymagane uwierzytelnianie.',
            ], 401);
        });

        $exceptions->render(function (ModelNotFoundException $e) {
            $model = class_basename($e->getModel());
            return response()->json([
                'success' => false,
                'message' => "Nie znaleziono zasobu: {$model}.",
            ], 404);
        });

        $exceptions->render(function (NotFoundHttpException $e) {
            return response()->json([
                'success' => false,
                'message' => 'Podany endpoint nie istnieje.',
            ], 404);
        });

        $exceptions->render(function (ThrottleRequestsException $e) {
            return response()->json([
                'success' => false,
                'message' => 'Zbyt wiele zapytan. Sprobuj ponownie pozniej.',
                'retry_after' => $e->getHeaders()['Retry-After'] ?? null,
            ], 429);
        });
    })
    ->create();

Testowanie API z PHPUnit#

Testy sa nieodzowna czescia profesjonalnego API. Laravel oferuje rozbudowane narzedzia do testow HTTP:

php artisan make:test Api/ArticleTest
<?php

namespace Tests\Feature\Api;

use App\Models\Article;
use App\Models\Category;
use App\Models\Tag;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Laravel\Sanctum\Sanctum;
use Tests\TestCase;

class ArticleTest extends TestCase
{
    use RefreshDatabase;

    private User $user;
    private Category $category;

    protected function setUp(): void
    {
        parent::setUp();

        $this->user = User::factory()->create();
        $this->category = Category::factory()->create();
    }

    /** @test */
    public function guest_can_list_published_articles(): void
    {
        Article::factory()->count(5)->create(['status' => 'published']);
        Article::factory()->count(2)->create(['status' => 'draft']);

        $response = $this->getJson('/api/v1/articles');

        $response->assertOk()
            ->assertJsonCount(5, 'data')
            ->assertJsonStructure([
                'data' => [
                    '*' => [
                        'id',
                        'type',
                        'attributes' => ['title', 'slug', 'excerpt', 'created_at'],
                        'relationships' => ['author'],
                        'links' => ['self'],
                    ],
                ],
                'meta' => ['total', 'per_page', 'current_page'],
                'links' => ['first', 'last', 'prev', 'next'],
            ]);
    }

    /** @test */
    public function authenticated_user_can_create_article(): void
    {
        Sanctum::actingAs($this->user, ['articles:create']);

        $tag = Tag::factory()->create();

        $payload = [
            'title' => 'Moj nowy artykul o Laravel',
            'content' => str_repeat('Tresc artykulu testowego. ', 10),
            'category_id' => $this->category->id,
            'tag_ids' => [$tag->id],
            'status' => 'published',
        ];

        $response = $this->postJson('/api/v1/articles', $payload);

        $response->assertCreated()
            ->assertJsonPath('data.attributes.title', 'Moj nowy artykul o Laravel')
            ->assertJsonPath('data.relationships.author.id', $this->user->id);

        $this->assertDatabaseHas('articles', [
            'title' => 'Moj nowy artykul o Laravel',
            'user_id' => $this->user->id,
        ]);
    }

    /** @test */
    public function unauthenticated_user_cannot_create_article(): void
    {
        $payload = [
            'title' => 'Artykul bez autoryzacji',
            'content' => str_repeat('Tresc artykulu. ', 10),
            'category_id' => $this->category->id,
        ];

        $response = $this->postJson('/api/v1/articles', $payload);

        $response->assertUnauthorized();
    }

    /** @test */
    public function it_validates_article_creation_data(): void
    {
        Sanctum::actingAs($this->user, ['articles:create']);

        $response = $this->postJson('/api/v1/articles', [
            'title' => '',
            'content' => 'Krotka',
        ]);

        $response->assertUnprocessable()
            ->assertJsonValidationErrors(['title', 'content', 'category_id']);
    }

    /** @test */
    public function user_can_update_own_article(): void
    {
        Sanctum::actingAs($this->user, ['articles:update']);

        $article = Article::factory()->create(['user_id' => $this->user->id]);

        $response = $this->putJson("/api/v1/articles/{$article->id}", [
            'title' => 'Zaktualizowany tytul',
        ]);

        $response->assertOk()
            ->assertJsonPath('data.attributes.title', 'Zaktualizowany tytul');
    }

    /** @test */
    public function user_cannot_delete_others_article(): void
    {
        $otherUser = User::factory()->create();
        Sanctum::actingAs($this->user, ['articles:delete']);

        $article = Article::factory()->create(['user_id' => $otherUser->id]);

        $response = $this->deleteJson("/api/v1/articles/{$article->id}");

        $response->assertForbidden();
    }

    /** @test */
    public function it_paginates_articles(): void
    {
        Article::factory()->count(25)->create(['status' => 'published']);

        $response = $this->getJson('/api/v1/articles?per_page=10&page=2');

        $response->assertOk()
            ->assertJsonCount(10, 'data')
            ->assertJsonPath('meta.current_page', 2)
            ->assertJsonPath('meta.per_page', 10);
    }
}

Uruchomienie testow:

# Wszystkie testy
php artisan test

# Tylko testy API
php artisan test --filter=ArticleTest

# Z pokryciem kodu
php artisan test --coverage --min=80

Dokumentacja API ze Scribe#

Automatyczna dokumentacja API znacznie ulatwia prace z API zarowno zespolowi deweloperow, jak i zewnetrznym konsumentom:

composer require knuckleswtf/scribe
php artisan vendor:publish --tag=scribe-config

Konfiguracja w config/scribe.php:

return [
    'title' => 'My API Documentation',
    'description' => 'Dokumentacja REST API',
    'base_url' => env('APP_URL'),
    'type' => 'external_static', // lub 'laravel'
    'auth' => [
        'enabled' => true,
        'default' => true,
        'in' => 'bearer',
        'name' => 'Authorization',
        'use_value' => 'Bearer {TOKEN}',
        'placeholder' => '{TOKEN}',
    ],
];

Dodaj adnotacje do kontrolerow:

/**
 * @group Artykuly
 *
 * Zarzadzanie artykulami w systemie.
 */
class ArticleController extends Controller
{
    /**
     * Lista artykulow
     *
     * Pobierz paginowana liste opublikowanych artykulow.
     * Obsluguje filtrowanie, sortowanie i wyszukiwanie.
     *
     * @queryParam search string Wyszukaj artykuly po tytule lub tresci. Example: Laravel
     * @queryParam tag string Filtruj po tagu. Example: php
     * @queryParam sort string Sortuj wyniki. Prefiks '-' oznacza malejaco. Example: -created_at
     * @queryParam per_page integer Liczba wynikow na strone (maks. 100). Example: 15
     * @queryParam page integer Numer strony. Example: 1
     *
     * @response 200 scenario="Success" {
     *   "data": [
     *     {
     *       "id": 1,
     *       "type": "article",
     *       "attributes": {
     *         "title": "Wprowadzenie do Laravel",
     *         "slug": "wprowadzenie-do-laravel",
     *         "excerpt": "Laravel to nowoczesny framework PHP..."
     *       }
     *     }
     *   ],
     *   "meta": {"total": 50, "per_page": 15, "current_page": 1}
     * }
     */
    public function index(Request $request): ArticleCollection
    {
        // ...
    }
}

Generowanie dokumentacji:

php artisan scribe:generate

Podsumowanie#

Budowanie REST API w Laravel z Sanctum to proces, ktory obejmuje wiele aspektow - od poprawnego projektowania endpointow, przez walidacje i uwierzytelnianie, az po testowanie i dokumentacje. Kluczowe elementy to:

  • Spójne API Resources do kontrolowanej transformacji danych
  • Laravel Sanctum do bezpiecznego uwierzytelniania tokenami i sesjami
  • Form Requests do czystej i reuzytecznej walidacji
  • Rate limiting do ochrony API przed naduzyciem
  • Wersjonowanie do zapewnienia kompatybilnosci wstecznej
  • Testy PHPUnit do weryfikacji poprawnosci dzialania
  • Automatyczna dokumentacja do ulatwienia integracji

Stosujac te praktyki, stworzysz API, ktore jest bezpieczne, wydajne i latwe w utrzymaniu.


Potrzebujesz profesjonalnego API dla swojego projektu?#

W MDS Software Solutions Group specjalizujemy sie w projektowaniu i budowaniu skalowalnych REST API z wykorzystaniem Laravel, .NET i Node.js. Nasz zespol doswiadczonych deweloperow pomoze Ci stworzyc bezpieczne, wydajne i dobrze udokumentowane API, ktore spelni wymagania Twojego biznesu.

Skontaktuj sie z nami i porozmawiajmy o Twoim projekcie. Oferujemy bezplatna konsultacje, podczas ktorej ocenimy Twoje potrzeby i zaproponujemy optymalne rozwiazanie.

Autor
MDS Software Solutions Group

Zespół ekspertów programistycznych specjalizujących się w nowoczesnych technologiach webowych.

Budowanie REST API w Laravel z Sanctum - kompletny przewodnik | MDS Software Solutions Group | MDS Software Solutions Group