Przejdź do treści
Backend

REST-APIs mit Laravel und Sanctum erstellen - ein vollstaendiger Leitfaden

Veröffentlicht am:
·4 Min. Lesezeit·Autor: MDS Software Solutions Group
REST-APIs mit Laravel und Sanctum erstellen - ein vollstaendiger Leitfaden

REST-APIs mit Laravel und Sanctum erstellen - ein vollstaendiger Leitfaden

Laravel ist eines der beliebtesten PHP-Frameworks und bietet elegante Werkzeuge fuer die Entwicklung moderner REST-APIs. In Kombination mit Laravel Sanctum - einem leichtgewichtigen Authentifizierungssystem - koennen Sie sichere, leistungsfaehige und skalierbare APIs in kurzer Zeit erstellen. In diesem Leitfaden fuehren wir Sie durch den gesamten Prozess: von der Projekteinrichtung ueber das Design von RESTful-Endpunkten bis hin zu Authentifizierung, Validierung, Tests und Dokumentation.

Ein Laravel-API-Projekt einrichten#

Beginnen Sie mit der Erstellung eines neuen Laravel-Projekts und konfigurieren Sie es fuer die API-Entwicklung:

# Neues Laravel-Projekt installieren
composer create-project laravel/laravel my-api-project
cd my-api-project

# .env mit Datenbankverbindung konfigurieren
# DB_CONNECTION=mysql
# DB_HOST=127.0.0.1
# DB_PORT=3306
# DB_DATABASE=my_api
# DB_USERNAME=root
# DB_PASSWORD=secret

# Migrationen ausfuehren
php artisan migrate

Laravel wird standardmaessig mit der Datei routes/api.php ausgeliefert, in der Sie Ihre API-Routen definieren. Alle Routen in dieser Datei erhalten automatisch das Praefix /api und verwenden die api-Middleware-Gruppe.

Projektstruktur fuer APIs#

Ein gut organisiertes Laravel-API-Projekt sollte eine klare Verzeichnisstruktur aufweisen:

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

RESTful-API-Designprinzipien#

Bevor wir mit der Implementierung beginnen, ist es wichtig, die wesentlichen Prinzipien des REST-API-Designs zu verstehen:

Namenskonventionen fuer Ressourcen#

Verwenden Sie Substantive im Plural fuer Ressourcennamen und HTTP-Verben fuer Aktionen:

| HTTP-Methode | Endpunkt | Beschreibung | |--------------|----------------------|-------------------------| | GET | /api/articles | Artikel auflisten | | GET | /api/articles/{id} | Einzelnen Artikel laden | | POST | /api/articles | Artikel erstellen | | PUT/PATCH | /api/articles/{id} | Artikel aktualisieren | | DELETE | /api/articles/{id} | Artikel loeschen |

HTTP-Statuscodes#

Verwenden Sie konsistent die richtigen HTTP-Statuscodes:

  • 200 OK - erfolgreicher Abruf oder Aktualisierung
  • 201 Created - erfolgreiche Ressourcenerstellung
  • 204 No Content - erfolgreiche Loeschung
  • 400 Bad Request - fehlerhafte Eingabe
  • 401 Unauthorized - fehlende Authentifizierung
  • 403 Forbidden - unzureichende Berechtigungen
  • 404 Not Found - Ressource nicht gefunden
  • 422 Unprocessable Entity - Validierungsfehler
  • 429 Too Many Requests - Rate-Limit ueberschritten
  • 500 Internal Server Error - Serverfehler

API Resource Controllers#

Laravel ermoeglicht es Ihnen, Controller mit einem vollstaendigen Satz von CRUD-Methoden mit einem einzigen Befehl zu erstellen:

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

Das Flag --api generiert einen Controller ohne die Methoden create und edit (HTML-Formulare), die fuer APIs nicht benoetigt werden:

<?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
{
    /**
     * Paginierte Liste der Artikel anzeigen.
     */
    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);
    }

    /**
     * Neuen Artikel speichern.
     */
    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);
    }

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

    /**
     * Artikel aktualisieren.
     */
    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']));
    }

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

        $article->delete();

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

Routenregistrierung#

Registrieren Sie den Controller in routes/api.php:

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

// Oeffentliche Routen
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']);
});

// Geschuetzte Routen
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 - Datentransformation#

API Resources bieten eine kontrollierte Moeglichkeit, Eloquent-Modelle in JSON-Antworten umzuwandeln. Sie dienen als Abstraktionsschicht zwischen Ihren Modellen und der API-Ausgabe:

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

Ressource definieren#

<?php

namespace App\Http\Resources;

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

class ArticleResource extends JsonResource
{
    /**
     * Ressource in ein Array transformieren.
     */
    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),
            ],
        ];
    }
}

Ressourcensammlung mit Metadaten#

<?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 - Einrichtung und Authentifizierung#

Laravel Sanctum bietet zwei Authentifizierungsmodi: API-Tokens fuer mobile Apps und externe Clients sowie sitzungsbasierte (Cookie-) Authentifizierung fuer SPAs.

Installation und Konfiguration#

# Sanctum ist seit Laravel 11 standardmaessig enthalten
# Fuer aeltere Versionen:
composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate

Stellen Sie sicher, dass Ihr User-Modell das Trait HasApiTokens verwendet:

<?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',
        ];
    }
}

Authentifizierungs-Controller mit Tokens#

<?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
{
    /**
     * Neuen Benutzer registrieren.
     */
    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 erfolgreich erstellt.',
            'user' => [
                'id' => $user->id,
                'name' => $user->name,
                'email' => $user->email,
            ],
            'token' => $token->plainTextToken,
            'token_type' => 'Bearer',
            'expires_at' => $token->accessToken->expires_at->toISOString(),
        ], 201);
    }

    /**
     * Benutzer anmelden und Token generieren.
     */
    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' => ['Die angegebenen Anmeldedaten sind ungueltig.'],
            ]);
        }

        // Optional: Alte Tokens widerrufen
        // $user->tokens()->delete();

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

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

    /**
     * Abmelden - aktuellen Token widerrufen.
     */
    public function logout(Request $request): JsonResponse
    {
        $request->user()->currentAccessToken()->delete();

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

    /**
     * Berechtigungen basierend auf der Benutzerrolle abrufen.
     */
    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'],
        };
    }
}

Token-Berechtigungen pruefen#

// Im Controller oder Middleware
public function store(StoreArticleRequest $request): JsonResponse
{
    if (! $request->user()->tokenCan('articles:create')) {
        abort(403, 'Sie haben keine Berechtigung, Artikel zu erstellen.');
    }

    // ... Erstellungslogik
}

SPA-Authentifizierung mit Sanctum#

Fuer Single Page Applications (React, Vue, Angular) bietet Sanctum eine Cookie-basierte Authentifizierung:

CORS- und Sanctum-Konfiguration#

In config/sanctum.php:

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

In config/cors.php:

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

SPA-Authentifizierungsablauf#

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

// 2. Anmelden
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. Nachfolgende Anfragen werden automatisch ueber Cookies authentifiziert
const articles = await fetch('/api/v1/articles', {
    headers: { 'Accept': 'application/json' },
    credentials: 'include',
});

API-Rate-Limiting#

Laravel bietet ein flexibles Rate-Limiting-System, das im AppServiceProvider definiert wird:

<?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
    {
        // Globales API-Limit
        RateLimiter::for('api', function (Request $request) {
            return Limit::perMinute(60)->by(
                $request->user()?->id ?: $request->ip()
            )->response(function () {
                return response()->json([
                    'message' => 'Zu viele Anfragen. Bitte versuchen Sie es spaeter erneut.',
                    'retry_after' => 60,
                ], 429);
            });
        });

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

        // Hoeheres Limit fuer authentifizierte Benutzer
        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);
        });
    }
}

Anwendung auf Routen:

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);
});

API-Versionierung#

Versionierung ermoeglicht es Ihnen, Aenderungen einzufuehren, ohne bestehende Clients zu beeintraechtigen:

URL-Praefix-Strategie#

// 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);
});

Header-basierte Strategie#

// 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);
    }
}

Verwendung eines abstrakten Basis-Controllers#

<?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);
    }
}

Request-Validierung mit Form Requests#

Form Requests sind eine elegante Methode, die Validierungslogik aus den Controllern auszulagern:

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' => 'Der Artikeltitel ist erforderlich.',
            'title.unique' => 'Ein Artikel mit diesem Titel existiert bereits.',
            'content.min' => 'Der Artikelinhalt muss mindestens :min Zeichen lang sein.',
            'category_id.exists' => 'Die ausgewaehlte Kategorie existiert nicht.',
        ];
    }

    /**
     * JSON-Antwort bei Validierungsfehler zurueckgeben.
     */
    protected function failedValidation(Validator $validator): void
    {
        throw new HttpResponseException(
            response()->json([
                'success' => false,
                'message' => 'Validierungsfehler.',
                'errors' => $validator->errors(),
            ], 422)
        );
    }
}

Fehlerbehandlung und API-Antworten#

Konfigurieren Sie die globale Fehlerbehandlung in 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' => 'Authentifizierung erforderlich.',
            ], 401);
        });

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

        $exceptions->render(function (NotFoundHttpException $e) {
            return response()->json([
                'success' => false,
                'message' => 'Der angegebene Endpunkt existiert nicht.',
            ], 404);
        });

        $exceptions->render(function (ThrottleRequestsException $e) {
            return response()->json([
                'success' => false,
                'message' => 'Zu viele Anfragen. Bitte versuchen Sie es spaeter erneut.',
                'retry_after' => $e->getHeaders()['Retry-After'] ?? null,
            ], 429);
        });
    })
    ->create();

APIs mit PHPUnit testen#

Tests sind ein wesentlicher Bestandteil professioneller API-Entwicklung. Laravel bietet leistungsstarke HTTP-Testwerkzeuge:

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' => 'Mein neuer Artikel ueber Laravel',
            'content' => str_repeat('Testinhalt fuer den Artikel. ', 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', 'Mein neuer Artikel ueber Laravel')
            ->assertJsonPath('data.relationships.author.id', $this->user->id);

        $this->assertDatabaseHas('articles', [
            'title' => 'Mein neuer Artikel ueber Laravel',
            'user_id' => $this->user->id,
        ]);
    }

    /** @test */
    public function unauthenticated_user_cannot_create_article(): void
    {
        $payload = [
            'title' => 'Unautorisierter Artikel',
            'content' => str_repeat('Artikelinhalt. ', 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' => 'Kurz',
        ]);

        $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' => 'Aktualisierter Titel',
        ]);

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

    /** @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);
    }
}

Tests ausfuehren:

# Alle Tests
php artisan test

# Nur API-Tests
php artisan test --filter=ArticleTest

# Mit Code-Coverage
php artisan test --coverage --min=80

API-Dokumentation mit Scribe#

Automatisierte API-Dokumentation erleichtert die Arbeit mit Ihrer API sowohl fuer Ihr Entwicklungsteam als auch fuer externe Nutzer erheblich:

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

Konfiguration in config/scribe.php:

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

Fuegen Sie Annotationen zu Ihren Controllern hinzu:

/**
 * @group Artikel
 *
 * Artikel im System verwalten.
 */
class ArticleController extends Controller
{
    /**
     * Artikel auflisten
     *
     * Paginierte Liste der veroeffentlichten Artikel abrufen.
     * Unterstuetzt Filterung, Sortierung und Suche.
     *
     * @queryParam search string Artikel nach Titel oder Inhalt suchen. Example: Laravel
     * @queryParam tag string Nach Tag-Slug filtern. Example: php
     * @queryParam sort string Ergebnisse sortieren. Praefix '-' fuer absteigend. Example: -created_at
     * @queryParam per_page integer Ergebnisse pro Seite (max. 100). Example: 15
     * @queryParam page integer Seitennummer. Example: 1
     *
     * @response 200 scenario="Erfolg" {
     *   "data": [
     *     {
     *       "id": 1,
     *       "type": "article",
     *       "attributes": {
     *         "title": "Einfuehrung in Laravel",
     *         "slug": "einfuehrung-in-laravel",
     *         "excerpt": "Laravel ist ein modernes PHP-Framework..."
     *       }
     *     }
     *   ],
     *   "meta": {"total": 50, "per_page": 15, "current_page": 1}
     * }
     */
    public function index(Request $request): ArticleCollection
    {
        // ...
    }
}

Dokumentation generieren:

php artisan scribe:generate

Zusammenfassung#

Die Erstellung von REST-APIs mit Laravel und Sanctum umfasst viele Aspekte - vom richtigen Endpunkt-Design ueber Validierung und Authentifizierung bis hin zu Tests und Dokumentation. Die wichtigsten Elemente sind:

  • Konsistente API Resources fuer kontrollierte Datentransformation
  • Laravel Sanctum fuer sichere Token- und sitzungsbasierte Authentifizierung
  • Form Requests fuer saubere, wiederverwendbare Validierungslogik
  • Rate Limiting zum Schutz Ihrer API vor Missbrauch
  • Versionierung zur Sicherstellung der Abwaertskompatibilitaet
  • PHPUnit-Tests zur Ueberpruefung der korrekten Funktionsweise
  • Automatisierte Dokumentation zur Vereinfachung der Integration

Wenn Sie diese Praktiken befolgen, erstellen Sie APIs, die sicher, leistungsfaehig und einfach zu warten sind.


Benoetigen Sie eine professionelle API fuer Ihr Projekt?#

Bei MDS Software Solutions Group sind wir auf die Konzeption und Entwicklung skalierbarer REST-APIs mit Laravel, .NET und Node.js spezialisiert. Unser Team erfahrener Entwickler hilft Ihnen, sichere, leistungsfaehige und gut dokumentierte APIs zu erstellen, die Ihren geschaeftlichen Anforderungen entsprechen.

Kontaktieren Sie uns, um Ihr Projekt zu besprechen. Wir bieten eine kostenlose Beratung an, bei der wir Ihre Anforderungen bewerten und eine optimale Loesung vorschlagen.

Autor
MDS Software Solutions Group

Team von Programmierexperten, die sich auf moderne Webtechnologien spezialisiert haben.

REST-APIs mit Laravel und Sanctum erstellen - ein vollstaendiger Leitfaden | MDS Software Solutions Group | MDS Software Solutions Group