Laravel-API-Guide
Du kennst die API-Architekturen und willst sie in Laravel sauber umsetzen. Dieser Guide zeigt pro Architektur die Laravel-typischen Bausteine, die empfohlenen Pakete und die wichtigsten Best Practices.
Laravel 11+. Die Beispiele nutzen PHP 8.3-Syntax (Constructor Property Promotion, readonly, enums).
1. REST β das Brot-und-Butter-Patternβ
Bausteineβ
- Routes:
routes/api.phpmitRoute::apiResource - Controller: Single-Action Controllers (Invokable) oder Resource-Controller
- FormRequest: Validation
- API-Resource: Output-Transformation
- Policy: Authorization
Routenβ
// routes/api.php
use App\Http\Controllers\Api\OrderController;
Route::middleware(['auth:sanctum', 'throttle:60,1'])->group(function () {
Route::apiResource('orders', OrderController::class);
Route::post('orders/{order}/cancel', [OrderController::class, 'cancel']);
});
Controllerβ
// app/Http/Controllers/Api/OrderController.php
final class OrderController extends Controller
{
public function index(IndexOrderRequest $request): AnonymousResourceCollection
{
$this->authorize('viewAny', Order::class);
$orders = Order::query()
->forUser($request->user())
->filter($request->validated())
->latest()
->paginate(perPage: 25);
return OrderResource::collection($orders);
}
public function store(StoreOrderRequest $request): JsonResponse
{
$order = app(CreateOrderAction::class)->execute(
user: $request->user(),
data: $request->validated(),
);
return OrderResource::make($order)
->response()
->setStatusCode(201);
}
}
FormRequest + Resourceβ
final class StoreOrderRequest extends FormRequest
{
public function rules(): array
{
return [
'items' => ['required', 'array', 'min:1'],
'items.*.product_id' => ['required', 'integer', 'exists:products,id'],
'items.*.quantity' => ['required', 'integer', 'min:1', 'max:100'],
'notes' => ['nullable', 'string', 'max:500'],
];
}
}
final class OrderResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'status' => $this->status->value,
'total' => $this->total->format(),
'items' => OrderItemResource::collection($this->whenLoaded('items')),
'created' => $this->created_at->toIso8601String(),
'links' => [
'self' => route('orders.show', $this->resource),
'cancel' => route('orders.cancel', $this->resource),
],
];
}
}
Best Practicesβ
- Action-Klassen statt Fat-Controller-Methoden (
CreateOrderAction,CancelOrderAction) - Konsistente Fehlerformate via
App\Exceptions\Handler(RFC 7807 / JSON:API) - OpenAPI mit
darkaonline/l5-swaggeroder Scribe automatisch generieren - API-Versionierung ΓΌber URL-Prefix (
/api/v1/) oderAccept-Header - Pagination nur via
paginate()(Cursor fΓΌr unendliche Listen:cursorPaginate()) - Rate Limiting mit benannten Limitern in
RouteServiceProvider - Idempotency-Key-Header fΓΌr POSTs prΓΌfen (gegen Doppel-Submits)
Paketeβ
- Laravel Sanctum β Token-Auth fΓΌr SPAs/Mobile
- Laravel Passport β OAuth2 Server
- Scribe β API-Doku-Generator
- Spatie Query Builder β Filter/Sort/Include aus Query-Params
2. GraphQLβ
Empfohlener Stack: Lighthouseβ
composer require nuwave/lighthouse
php artisan lighthouse:install
Schema-Firstβ
# graphql/schema.graphql
type Query {
user(id: ID! @eq): User @find
orders(
status: OrderStatus @eq
first: Int! = 25
page: Int
): [Order!]! @paginate(defaultCount: 25)
}
type Mutation {
createOrder(input: CreateOrderInput! @spread): Order!
@field(resolver: "App\\GraphQL\\Mutations\\CreateOrder")
@guard
}
type Order {
id: ID!
status: OrderStatus!
total: String!
items: [OrderItem!]! @hasMany
user: User! @belongsTo
}
enum OrderStatus {
PENDING
PAID
SHIPPED
CANCELLED
}
input CreateOrderInput {
items: [OrderItemInput!]!
notes: String
}
Resolverβ
final class CreateOrder
{
public function __construct(
private readonly CreateOrderAction $action,
) {}
public function __invoke(mixed $_, array $args, GraphQLContext $ctx): Order
{
return $this->action->execute(
user: $ctx->user(),
data: $args,
);
}
}
Best Practicesβ
- N+1 vermeiden mit
@hasMany,@belongsTo(Lighthouse lΓΆst das via DataLoader) oder eigenemBatchLoader - Query-KomplexitΓ€t limitieren:
lighthouse.security.max_query_depthundmax_query_complexity - Persisted Queries im Frontend (Apollo) β schΓΌtzt vor DoS und reduziert Payload
- Authorization:
@guard,@can, eigeneFieldMiddleware - Subscriptions via Laravel Reverb / Pusher / Redis
- GraphiQL im Dev-Modus aktivieren, in Production deaktivieren
- Strict Schema: Niemals
Any-Scalar β immer typisieren
Alternativeβ
- Lighthouse β Schema-First, Defacto-Standard
- GraphQLite β Code-First mit Annotations
- rebing/graphql-laravel β Code-First klassisch
3. gRPC β Service-zu-Serviceβ
Empfohlener Stack: Spiral RoadRunner + spiral/php-grpcβ
PHP-FPM kann gRPC nicht effizient bedienen (kein HTTP/2-Multiplexing). RoadRunner (Go-basierter App-Server) lΓΆst das.
composer require spiral/roadrunner-grpc spiral/php-grpc
Protoβ
// proto/order.proto
syntax = "proto3";
package billing;
service OrderService {
rpc GetOrder (OrderRequest) returns (Order);
rpc StreamOrders (OrderFilter) returns (stream Order);
}
message OrderRequest { int32 id = 1; }
message Order {
int32 id = 1;
string status = 2;
double total = 3;
}
Generierung & Service-Implementierungβ
protoc --php_out=app/Grpc --grpc-php_out=app/Grpc proto/order.proto
final class OrderServiceImpl implements OrderServiceInterface
{
public function __construct(
private readonly OrderRepository $orders,
) {}
public function GetOrder(GRPC\ContextInterface $ctx, OrderRequest $in): Order
{
$order = $this->orders->find($in->getId())
?? throw new GRPCException('Order not found', StatusCode::NOT_FOUND);
return (new Order())
->setId($order->id)
->setStatus($order->status->value)
->setTotal($order->total->amount());
}
}
Best Practicesβ
- mTLS fΓΌr interne Service-Calls β RoadRunner unterstΓΌtzt es nativ
- Proto-Files in eigenes Repo (oder
packages/proto) β versioniert - Backward Compatibility: Felder nie umnummerieren, nur hinzufΓΌgen
- Deadlines & Cancellation clientseitig immer setzen
- gRPC fΓΌr intern β nach auΓen weiterhin REST/GraphQL als Edge
4. WebSocket β Echtzeit mit Laravel Reverbβ
Reverb ist Laravels offizieller, eigener WebSocket-Server (ab Laravel 11). Pusher-API-kompatibel, lokal lauffΓ€hig.
composer require laravel/reverb
php artisan reverb:install
php artisan reverb:start
Broadcasting-Eventβ
final class OrderShipped implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(public readonly Order $order) {}
public function broadcastOn(): array
{
return [new PrivateChannel("orders.{$this->order->id}")];
}
public function broadcastWith(): array
{
return [
'tracking_url' => $this->order->tracking_url,
'shipped_at' => $this->order->shipped_at->toIso8601String(),
];
}
}
Channel-Authβ
// routes/channels.php
Broadcast::channel('orders.{order}', function (User $user, Order $order) {
return $user->id === $order->user_id;
});
Client (Laravel Echo)β
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';
window.Echo = new Echo({
broadcaster: 'reverb',
key: import.meta.env.VITE_REVERB_APP_KEY,
wsHost: import.meta.env.VITE_REVERB_HOST,
wsPort: 8080,
forceTLS: false,
});
Echo.private(`orders.${orderId}`)
.listen('OrderShipped', (e) => updateUI(e));
Best Practicesβ
- PrivateChannel / PresenceChannel mit Auth, nie ΓΆffentlich fΓΌr Userdaten
- Reverb hinter Reverse Proxy (Nginx mit
proxy_set_header Upgrade) - Skalierung horizontal via Redis-Pub/Sub-Backend
- Reconnect-Strategien im Echo-Client konfigurieren
- Whisper (
.whisper()) fΓΌr Client-zu-Client-Events (Typing-Indicators, Cursors) - Heartbeat ΓΌber
pings_intervalkonfigurieren
Alternativenβ
- Soketi β externer Pusher-kompatibler Server (Node)
- Pusher Channels (SaaS)
- Ably (SaaS)
5. Server-Sent Events β LLM-Streaming, Notificationsβ
Laravel kann SSE ΓΌber StreamedResponse direkt liefern β kein Reverb nΓΆtig.
final class LlmStreamController
{
public function __invoke(StreamRequest $request, ClaudeClient $claude): StreamedResponse
{
return response()->stream(function () use ($request, $claude) {
foreach ($claude->stream($request->validated()) as $chunk) {
echo "data: " . json_encode($chunk) . "\n\n";
ob_flush();
flush();
}
echo "event: done\ndata: {}\n\n";
}, headers: [
'Content-Type' => 'text/event-stream',
'Cache-Control' => 'no-cache',
'X-Accel-Buffering' => 'no', // Nginx-Buffering aus
'Connection' => 'keep-alive',
]);
}
}
Best Practicesβ
- PHP-FPM Timeout und
max_execution_timehochsetzen (oder via Octane) - Nginx:
proxy_buffering offfΓΌr SSE-Routen - Frontend
EventSourcemit automatischem Reconnect nutzen - Authentifizierung ΓΌber Query-Param oder Cookie β
EventSourceunterstΓΌtzt keine Custom-Header - Heartbeat-Events alle 15s, damit Proxies die Verbindung nicht killen
- Octane oder FrankenPHP in Production β klassisches PHP-FPM blockiert sonst Worker
6. Webhooks β eingehendβ
Ein eingehender Webhook (z. B. Stripe)β
// routes/api.php
Route::post('webhooks/stripe', StripeWebhookController::class)
->withoutMiddleware([VerifyCsrfToken::class]);
final class StripeWebhookController
{
public function __invoke(Request $request): Response
{
try {
$event = Webhook::constructEvent(
$request->getContent(),
$request->header('Stripe-Signature'),
config('services.stripe.webhook_secret'),
);
} catch (SignatureVerificationException) {
abort(401, 'Invalid signature');
}
// Idempotenz: jeden Stripe-Event-Hash 1Γ behandeln
if (WebhookLog::where('external_id', $event->id)->exists()) {
return response('Already processed', 200);
}
ProcessStripeEvent::dispatch($event->toArray());
WebhookLog::create(['external_id' => $event->id, 'type' => $event->type]);
return response('OK', 200);
}
}
Best Practicesβ
- Signatur verifizieren β bei JEDEM Webhook
- Idempotenz ΓΌber Event-ID-Tracking
- Sofort 2xx zurΓΌckgeben, schwere Arbeit in Queue-Job verschieben
- Logging in eigener Tabelle (
webhook_logs) fΓΌr Replay/Audit - Spatie Webhook Client als fertiges Paket (spatie/laravel-webhook-client)
Ausgehende Webhooks (du verschickst)β
β spatie/laravel-webhook-server β HMAC-Signing, Retries, Backoff, Worker.
7. SOAPβ
PHP hat eingebauten SOAP-Support (ext-soap). In Laravel meist als Wrapper im Service.
final class ElsterClient
{
private SoapClient $client;
public function __construct(string $wsdl)
{
$this->client = new SoapClient($wsdl, [
'trace' => true,
'exceptions' => true,
'soap_version' => SOAP_1_2,
'cache_wsdl' => WSDL_CACHE_BOTH,
]);
}
public function submitReport(array $data): SubmitResult
{
try {
$response = $this->client->__soapCall('Submit', [$data]);
return SubmitResult::fromResponse($response);
} catch (SoapFault $e) {
Log::error('SOAP fault', [
'fault' => $e->getMessage(),
'request' => $this->client->__getLastRequest(),
'response' => $this->client->__getLastResponse(),
]);
throw new ElsterUnavailable($e->getMessage(), previous: $e);
}
}
}
Best Practicesβ
- WSDL lokal cachen (
cache_wsdl => WSDL_CACHE_BOTH) __getLastRequest/ResponsefΓΌr Debugging einschalten- Domain-Exceptions statt rohe
SoapFaults nach oben werfen - WSSecurity / Signing nur via offizielles Paket (robrichards/wse-php)
- Tests gegen WSDL-Mock (phpro/soap-client kann das)
8. tRPC-Bridge β Laravel als Backend fΓΌr TS-Frontendβ
tRPC ist TS-only. Wenn du tRPC-Style-Type-Safety mit Laravel-Backend willst:
- Spatie Laravel Data + Spatie Typescript-Transformer generiert TS-Typen aus Laravel-Klassen.
- Inertia.js umgeht klassische APIs komplett β Laravel rendert Props, Vue/React empfΓ€ngt sie typsicher.
// app/Data/OrderData.php
final class OrderData extends Data
{
public function __construct(
public readonly int $id,
public readonly OrderStatus $status,
public readonly string $total,
/** @var DataCollection<int, OrderItemData> */
public readonly DataCollection $items,
) {}
}
β php artisan typescript:transform produziert eine resources/types/generated.d.ts mit OrderData als TS-Interface. Frontend nutzt es direkt.
9. Queues β asynchrone Workloadsβ
Laravel's Queue-Layer ist eines seiner stΓ€rksten Features.
Jobβ
final class ProcessStripeEvent implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 5;
public int $backoff = 30;
public int $timeout = 120;
public function __construct(public readonly array $event) {}
public function handle(StripeEventHandler $handler): void
{
$handler->process($this->event);
}
public function uniqueId(): string
{
return $this->event['id']; // Idempotenz!
}
}
Best Practicesβ
- Treiber-Wahl: Redis (default), SQS fΓΌr Cloud, RabbitMQ fΓΌr komplexe Routings
- Horizon fΓΌr Redis-Queues β Dashboard, Auto-Scaling, Metriken
- Backoff exponentiell bei Retry:
[10, 30, 60, 120, 300] ShouldBeUniquefΓΌr Idempotenz auf Lock-Ebene- Batches fΓΌr Job-Gruppen mit Callback-Logik
- Job-Middleware fΓΌr Throttling externer APIs (
new RateLimited('stripe-api')) - Failed-Jobs-Table ΓΌberwachen / Alerts setzen
- Tests:
Queue::fake()undBus::fake()
10. Event-Streaming mit Kafka / Redpandaβ
FΓΌr Event-Sourcing oder Hochlast-Pipelines.
composer require mateusjunges/laravel-kafka
// Producer
Kafka::publishOn('orders.events')
->withHeaders(['event-type' => 'OrderCreated'])
->withBodyKey('order_id', $order->id)
->withBodyKey('total', $order->total->amount())
->send();
// Consumer (in Artisan-Command)
Kafka::consumer(['orders.events'])
->withHandler(function (ConsumedMessage $message) {
// β¦
})
->build()
->consume();
Best Practicesβ
- Schemas mit Avro oder Protobuf, registriert im Schema-Registry
- Consumer-Groups fΓΌr horizontale Skalierung
- Idempotenz im Consumer (z. B. Outbox-Pattern fΓΌr Producer)
- Dead-Letter-Topic fΓΌr unbehandelbare Nachrichten
11. Gesamtarchitektur in Laravel β ein Beispielβ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Edge / Public β
β REST API (Sanctum + Scribe-Doku) routes/api.php β
β GraphQL fΓΌr SPA /graphql via Lighthouse β
β Webhooks von Stripe/GitHub routes/api.php (signed) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Realtime β
β WebSocket (Reverb) fΓΌr Live-UI Broadcasting Events β
β SSE fΓΌr LLM-Streaming StreamedResponse + Octane β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Service-zu-Service β
β gRPC (RoadRunner) zu Pricing-Service proto/*.proto β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Async / Background β
β Redis-Queue + Horizon (Jobs) app/Jobs β
β Kafka (Event-Stream) Outbox-Pattern β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Legacy β
β SOAP-Client zu ELSTER app/Services/Elster* β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
12. Cross-Cutting Best Practices in Laravelβ
Authenticationβ
- SPA-Same-Domain: Sanctum Cookie-Mode
- Mobile / 3rd Party: Sanctum Personal Access Tokens oder Passport OAuth2
- Service-zu-Service: mTLS (gRPC) oder kurzlebige JWTs
Validationβ
- FormRequest-Klassen fΓΌr REST/GraphQL-Inputs
- Spatie Data fΓΌr typisierte DTOs
- Niemals Request-Daten direkt ins Eloquent-Model ohne Validation
- Enum-Casts fΓΌr Status-Felder
Authorizationβ
- Policies fΓΌr Eloquent-Modelle
- Gates fΓΌr globale Permissions
AuthorizeResourcein Controllern,@guard/@canin GraphQL
Fehlerformat (RFC 7807)β
// app/Exceptions/Handler.php
$this->renderable(function (Throwable $e, Request $request) {
if (! $request->expectsJson()) return null;
return match (true) {
$e instanceof ValidationException => response()->json([
'type' => 'https://example.com/errors/validation',
'title' => 'Validation failed',
'status' => 422,
'errors' => $e->errors(),
], 422),
$e instanceof ModelNotFoundException => response()->json([
'type' => 'https://example.com/errors/not-found',
'title' => 'Resource not found',
'status' => 404,
], 404),
default => null,
};
});
Logging & Observabilityβ
- Structured Logging (JSON-Format in Production)
- Telescope in Dev / Staging
- Sentry oder Bugsnag fΓΌr Errors
- OpenTelemetry ΓΌber open-telemetry/opentelemetry-laravel
- Per-Request-Trace-ID als Middleware (
X-Request-Id)
Testingβ
// Pest
it('creates an order', function () {
$user = User::factory()->create();
$response = $this->actingAs($user)->postJson('/api/orders', [
'items' => [['product_id' => 1, 'quantity' => 2]],
]);
$response->assertCreated()
->assertJsonPath('data.status', 'pending');
expect(Order::count())->toBe(1);
});
- Feature-Tests mit
actingAs()fΓΌr REST/GraphQL Event::fake(),Queue::fake(),Bus::fake()fΓΌr AsyncHttp::fake()fΓΌr externe APIs / Webhooks- gRPC: separate Tests gegen RoadRunner-Container in CI
Cachingβ
- HTTP-Caching via
etag()-Header undCache-Control - Eloquent-Cache sparsam β lieber gezielte
Cache::rememberin Actions - Tagged Cache mit Redis fΓΌr invalidierbare Gruppen
Rate Limitingβ
// app/Providers/RouteServiceProvider.php
RateLimiter::for('api', fn (Request $r) =>
Limit::perMinute(60)->by($r->user()?->id ?: $r->ip())
);
RateLimiter::for('webhooks', fn (Request $r) =>
Limit::perMinute(300)->by($r->ip())
);
API-Versionierungβ
- URL-Versionierung (
/api/v1/...) β am pragmatischsten - Header-Versionierung (
Accept: application/vnd.example.v2+json) β eleganter, schwerer cachebar - Niemals breaking changes ohne neue Version
- Deprecation-Header (
Sunset,Deprecation) im Γbergang
13. Pakete-Cheatsheetβ
| Zweck | Paket |
|---|---|
| REST-Doku | knuckleswtf/scribe, darkaonline/l5-swagger |
| Query-Builder mit Filtern | spatie/laravel-query-builder |
| GraphQL | nuwave/lighthouse |
| gRPC | spiral/php-grpc, roadrunner-server/grpc |
| WebSocket | laravel/reverb, soketi/soketi |
| Webhooks (in) | spatie/laravel-webhook-client |
| Webhooks (out) | spatie/laravel-webhook-server |
| Data Transfer Objects | spatie/laravel-data |
| TS-Codegen | spatie/typescript-transformer |
| Inertia | inertiajs/inertia-laravel |
| Queue-Dashboard | laravel/horizon |
| Kafka | mateusjunges/laravel-kafka |
| Octane (Performance) | laravel/octane |
| FrankenPHP | dunglas/frankenphp |
| OpenAPI-Validation | hkulekci/kafka-laravel |
14. Sicherheits-Checkliste (Laravel-spezifisch)β
APP_DEBUG=falsein Production β immer- HTTPS erzwingen via
URL::forceScheme('https')oder Trusted-Proxy-Konfig - CORS in
config/cors.phpexplizit β keine*fΓΌr Auth-Endpoints - Sanctum stateful domains korrekt konfiguriert
- CSRF fΓΌr Web-Routes aktiv, fΓΌr API-Routes ausgenommen
- Mass Assignment:
$guarded = []nur mit FormRequest-Validation davor β sonst$fillablewhitelisten - Eloquent Scope
forUser()ΓΌberall, wo Multi-Tenancy gilt - SQL-Injection via Query-Builder geschΓΌtzt β nie roh
DB::raw()mit User-Input - Webhook-Signaturen prΓΌfen (Stripe, GitHub, Mollie)
- Rate-Limiter auf JEDEM ΓΆffentlichen Endpoint
- Reverb: PrivateChannel mit Auth, niemals
ChannelfΓΌr Userdaten - gRPC: mTLS zwischen Services
15. WeiterfΓΌhrendβ
- API-Architekturen-Γberblick β API-Architekturen
- Live-OpenAPI-Playground β API Playground
- Laravel-Coding-Guidelines deines Teams β BSH-Skill
bsh-code-guideline
Externe Quellen
- Laravel Docs β API Resources
- Lighthouse GraphQL
- Laravel Reverb
- Spiral RoadRunner gRPC
- Spatie Packages
βJede API-Architektur hat in Laravel ein gutes Paket β die Kunst liegt nicht in der Implementierung, sondern in der Wahl der richtigen Architektur fΓΌr den richtigen Use-Case."