Een slecht ontworpen API kost je meer tijd dan hem goed bouwen. Versioning, pagination, rate limiting, consistente foutmeldingen — dit zijn de patronen die wij standaard toepassen.
Laravel API design patterns voor e-commerce
Een slecht ontworpen API betaal je twee keer: één keer bij de bouw en daarna elke sprint als je hem moet uitleggen, aanpassen of debuggen. E-commerce API's zijn extra gevoelig — ze koppelen webshops aan ERP systemen, voorraadbeheer, betaalproviders en mobiele apps tegelijk.
Dit artikel beschrijft de patronen die wij standaard toepassen. Geen academische theorie, maar beslissingen die je vandaag kunt nemen.
Wat je leert in dit artikel
- RESTful resource naamgeving die consistent blijft bij groei
- API versioning zonder je huidige clients te breken
- Paginering, filtering en sortering op een uniforme manier
- Rate limiting per client type
- Consistente foutresponses die integraties makkelijker maken
- Authentication best practices voor e-commerce API's
RESTful naamgeving: resources, niet acties
De meest gemaakte fout in API design: URL's die methoden beschrijven in plaats van resources.
// Fout: acties in de URL
POST /api/createOrder
GET /api/getOrderById/123
POST /api/cancelOrder
POST /api/markOrderAsShipped
// Correct: resources + HTTP methoden
POST /api/v1/orders // Maak order aan
GET /api/v1/orders/123 // Haal order op
DELETE /api/v1/orders/123 // Annuleer order (logisch verwijderen)
PATCH /api/v1/orders/123 // Wijzig status naar shipped
HTTP methoden dragen de betekenis. GET is altijd leesoperatie, nooit muterend. POST maakt aan. PUT vervangt volledig. PATCH muteert gedeeltelijk. DELETE verwijdert of deactiveert.
Voor geneste resources gebruik je dit patroon:
GET /api/v1/orders/123/lines // Orderregels van order 123
POST /api/v1/orders/123/lines // Voeg orderregel toe
GET /api/v1/orders/123/lines/45 // Specifieke orderregel
DELETE /api/v1/orders/123/lines/45 // Verwijder orderregel
Maximaal twee niveaus diep. Dieper dan dat wordt de URL onleesbaar en de structuur onhandelbaar.
Versioning: breek nooit bestaande clients
E-commerce API's koppelen aan systemen die je niet beheert. Een ERP integratie of externe bestelapp kan je niet dwingen om simultaan te updaten. Versioning beschermt die integraties.
URL versioning — onze voorkeur
// routes/api.php
Route::prefix('v1')->middleware('api')->group(function () {
Route::apiResource('orders', V1\OrderController::class);
Route::apiResource('products', V1\ProductController::class);
});
Route::prefix('v2')->middleware('api')->group(function () {
Route::apiResource('orders', V2\OrderController::class);
Route::apiResource('products', V2\ProductController::class);
});
URL versioning is expliciet en debugbaar. Je ziet meteen welke versie een client gebruikt in je logs.
Deprecation headers toevoegen
Geef clients tijd om te migreren door een deprecation waarschuwing mee te sturen.
// app/Http/Middleware/ApiVersionCheck.php
class ApiVersionCheck
{
public function handle(Request $request, Closure $next, string $version)
{
$response = $next($request);
if ($version === 'v1') {
$response->headers->set('Deprecation', 'true');
$response->headers->set('Sunset', 'Sat, 31 Dec 2025 23:59:59 GMT');
$response->headers->set('Link', '<https://api.jouwshop.nl/v2/docs>; rel="successor-version"');
}
return $response;
}
}
Resources en consistente responses
Gebruik Laravel API Resources voor alle responses. Nooit direct Eloquent models returnen — dat koppelt je database structuur aan je API contract.
// app/Http/Resources/OrderResource.php
class OrderResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'reference' => $this->reference,
'status' => $this->status,
'total' => [
'amount' => $this->total,
'currency' => 'EUR',
'formatted' => '€ ' . number_format($this->total, 2, ',', '.'),
],
'customer' => new CustomerResource($this->whenLoaded('customer')),
'lines' => OrderLineResource::collection($this->whenLoaded('lines')),
'created_at' => $this->created_at->toISOString(),
'updated_at' => $this->updated_at->toISOString(),
];
}
}
Consistente foutstructuur
Alle fouten hebben dezelfde structuur. Integraties hoeven maar één foutparser te schrijven.
// app/Exceptions/Handler.php
public function render($request, Throwable $e): Response
{
if ($request->expectsJson()) {
if ($e instanceof ValidationException) {
return response()->json([
'error' => [
'code' => 'VALIDATION_ERROR',
'message' => 'De invoer is ongeldig.',
'details' => $e->errors(),
],
], 422);
}
if ($e instanceof ModelNotFoundException) {
return response()->json([
'error' => [
'code' => 'NOT_FOUND',
'message' => 'De gevraagde resource bestaat niet.',
],
], 404);
}
if ($e instanceof AuthorizationException) {
return response()->json([
'error' => [
'code' => 'FORBIDDEN',
'message' => 'Je hebt geen toegang tot deze resource.',
],
], 403);
}
}
return parent::render($request, $e);
}
Paginering, filtering en sortering
Geen API endpoint mag een ongelimiteerde collectie returnen. Bij duizenden orders of producten breekt dat clients en belast het de database onnodig.
// app/Http/Controllers/Api/V1/OrderController.php
class OrderController extends Controller
{
public function index(Request $request): AnonymousResourceCollection
{
$validated = $request->validate([
'status' => ['nullable', 'string', Rule::in(['pending', 'processing', 'shipped', 'completed'])],
'customer_id' => ['nullable', 'integer', 'exists:customers,id'],
'date_from' => ['nullable', 'date'],
'date_to' => ['nullable', 'date', 'after_or_equal:date_from'],
'sort_by' => ['nullable', Rule::in(['created_at', 'total', 'reference'])],
'sort_dir' => ['nullable', Rule::in(['asc', 'desc'])],
'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
]);
$orders = Order::query()
->with(['customer', 'lines'])
->when($validated['status'] ?? null, fn($q, $v) => $q->where('status', $v))
->when($validated['customer_id'] ?? null, fn($q, $v) => $q->where('customer_id', $v))
->when($validated['date_from'] ?? null, fn($q, $v) => $q->whereDate('created_at', '>=', $v))
->when($validated['date_to'] ?? null, fn($q, $v) => $q->whereDate('created_at', '<=', $v))
->orderBy($validated['sort_by'] ?? 'created_at', $validated['sort_dir'] ?? 'desc')
->paginate($validated['per_page'] ?? 25);
return OrderResource::collection($orders);
}
}
Laravel's paginate() geeft automatisch paginametadata terug in de response:
{
"data": [...],
"links": {
"first": "https://api.jouwshop.nl/v1/orders?page=1",
"last": "https://api.jouwshop.nl/v1/orders?page=12",
"prev": null,
"next": "https://api.jouwshop.nl/v1/orders?page=2"
},
"meta": {
"current_page": 1,
"per_page": 25,
"total": 291,
"last_page": 12
}
}
Rate limiting per client type
Een ERP systeem heeft andere behoeften dan een mobiele app. Stel limieten in per type integratie.
// app/Providers/RouteServiceProvider.php
protected function configureRateLimiting(): void
{
RateLimiter::for('erp', function (Request $request) {
// ERP systemen mogen meer — ze zijn vertrouwde, interne integraties
return $request->user()?->isErpClient()
? Limit::perMinute(300)->by($request->user()->id)
: Limit::perMinute(60)->by($request->user()?->id ?? $request->ip());
});
RateLimiter::for('public-api', function (Request $request) {
return Limit::perMinute(30)
->by($request->user()?->id ?? $request->ip())
->response(function () {
return response()->json([
'error' => [
'code' => 'RATE_LIMIT_EXCEEDED',
'message' => 'Te veel verzoeken. Probeer het later opnieuw.',
],
], 429);
});
});
}
// routes/api.php
Route::middleware(['auth:sanctum', 'throttle:erp'])->prefix('v1')->group(function () {
Route::apiResource('orders', OrderController::class);
});
Idempotency voor bestellingen en betalingen
Een netwerkfout kan ervoor zorgen dat een client hetzelfde request twee keer stuurt. Voor orderaanmaak of betalingen is dat een serieus probleem. Idempotency keys lossen dit op.
// app/Http/Middleware/EnsureIdempotency.php
class EnsureIdempotency
{
public function handle(Request $request, Closure $next)
{
if (in_array($request->method(), ['POST', 'PUT', 'PATCH'])) {
$idempotencyKey = $request->header('Idempotency-Key');
if ($idempotencyKey) {
$cacheKey = "idempotency:{$idempotencyKey}";
if (Cache::has($cacheKey)) {
// Return de gecachte response — geen dubbele verwerking
return response()->json(Cache::get($cacheKey), 200)
->header('X-Idempotency-Replayed', 'true');
}
$response = $next($request);
// Sla response op voor 24 uur
Cache::put($cacheKey, $response->getData(), now()->addHours(24));
return $response;
}
}
return $next($request);
}
}
Clients sturen een unieke Idempotency-Key header bij elk muterend verzoek. Bij een dubbel verzoek krijgen ze de originele response terug zonder dat de actie opnieuw wordt uitgevoerd.
Conclusie
Een goed ontworpen API bespaart tijd bij elke integratie die je daarna bouwt. Consistente naamgeving, versioning en foutresponses zijn niet mooi om te hebben — ze verminderen bugs en support vragen.
De patronen in dit artikel zijn de basis van elke API die wij bouwen. Ze zijn niet nieuw, maar ze worden lang niet altijd consequent toegepast.
De patronen in dit artikel volgen de JSON:API specification en RESTful API design best practices. Zie ook de Laravel API Resources documentatie voor de implementatiedetails.
Gerelateerde artikelen:- API-beveiliging voor e-commerce integraties — security best practices
- Laravel Sanctum vs Passport — API authenticatie kiezen
- Laravel als e-commerce middleware — architectuurpatronen
- Event-driven architectuur in Laravel — asynchrone verwerking
Bekijk onze Laravel-diensten of neem contact op voor een code review of architectuurgesprek.

Geschreven door Ruthger Idema
15+ jaar ervaring in e-commerce development. Gespecialiseerd in Magento, Shopify en Laravel maatwerk.
Meer over ons team →