Building Scalable APIs with Laravel
Architecture and techniques for building production-grade RESTful APIs with Laravel — Repository Pattern, Queue Jobs, API versioning, rate limiting, and caching strategy.
Laravel is the leading PHP framework for building API backends. With a rich ecosystem and expressive syntax, Laravel enables small teams to ship production-ready APIs rapidly. This article shares the architecture and techniques Ventra Rocket uses to build APIs that serve millions of requests per day.
1. Repository Pattern for Business Logic
Decoupling database logic from controllers makes code testable and maintainable.
// app/Repositories/Contracts/OrderRepositoryInterface.php
interface OrderRepositoryInterface
{
public function findById(int $id): ?Order;
public function findByCustomer(int $customerId, array $filters = []): Collection;
public function create(array $data): Order;
public function updateStatus(int $id, string $status): bool;
}
// app/Repositories/EloquentOrderRepository.php
class EloquentOrderRepository implements OrderRepositoryInterface
{
public function findByCustomer(int $customerId, array $filters = []): Collection
{
return Order::query()
->where('customer_id', $customerId)
->when(isset($filters['status']), fn($q) => $q->where('status', $filters['status']))
->when(isset($filters['from']), fn($q) => $q->where('created_at', '>=', $filters['from']))
->with(['items', 'shipping'])
->latest()
->get();
}
}
// Bind in AppServiceProvider
$this->app->bind(OrderRepositoryInterface::class, EloquentOrderRepository::class);
2. API Versioning
Versioning is mandatory for maintaining backward compatibility as your API evolves.
// routes/api.php
Route::prefix('v1')->name('api.v1.')->group(function () {
Route::apiResource('orders', V1\OrderController::class);
Route::apiResource('products', V1\ProductController::class);
});
Route::prefix('v2')->name('api.v2.')->group(function () {
Route::apiResource('orders', V2\OrderController::class);
});
API Resources for Response Transformation
// app/Http/Resources/V2/OrderResource.php
class OrderResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'status' => $this->status,
'total' => number_format($this->total_amount, 2),
'currency' => $this->currency ?? 'AUD',
'items' => OrderItemResource::collection($this->whenLoaded('items')),
'customer' => new CustomerResource($this->whenLoaded('customer')),
'created_at' => $this->created_at->toIso8601String(),
'links' => [
'self' => route('api.v2.orders.show', $this->id),
'cancel' => $this->canCancel()
? route('api.v2.orders.cancel', $this->id)
: null,
],
];
}
}
3. Queue Jobs for Heavy Processing
Never process heavy tasks within the request cycle. Push them to a queue.
// app/Jobs/ProcessOrderFulfillment.php
class ProcessOrderFulfillment implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 3;
public int $backoff = 60;
public function __construct(private readonly Order $order) {}
public function handle(
InventoryService $inventory,
ShippingService $shipping,
NotificationService $notifier
): void {
DB::transaction(function () use ($inventory, $shipping, $notifier) {
$inventory->reserve($this->order->items);
$trackingCode = $shipping->createShipment($this->order);
$this->order->update([
'status' => 'processing',
'tracking_code' => $trackingCode,
]);
$notifier->sendOrderConfirmation($this->order);
});
}
public function failed(Throwable $exception): void
{
Log::error('Order fulfillment failed', [
'order_id' => $this->order->id,
'error' => $exception->getMessage(),
]);
$this->order->update(['status' => 'failed']);
}
}
4. Rate Limiting
Protect your API from abuse with flexible rate limiting.
// app/Providers/RouteServiceProvider.php
RateLimiter::for('api', function (Request $request) {
return $request->user()
? Limit::perMinute(120)->by($request->user()->id)
: Limit::perMinute(30)->by($request->ip());
});
RateLimiter::for('heavy-operations', function (Request $request) {
return [
Limit::perHour(10)->by($request->user()?->id ?? $request->ip()),
Limit::perDay(50)->by($request->user()?->id ?? $request->ip()),
];
});
5. Caching Strategy
class ProductCatalogService
{
public function getByCategory(int $categoryId): Collection
{
return Cache::tags(['products', "category:{$categoryId}"])
->remember(
"products.category.{$categoryId}",
now()->addHours(2),
fn () => Product::active()
->inCategory($categoryId)
->with(['images', 'variants'])
->get()
);
}
public function invalidateCategory(int $categoryId): void
{
Cache::tags(["category:{$categoryId}"])->flush();
}
}
6. Error Handling per RFC 7807
public function register(): void
{
$this->renderable(function (ValidationException $e, Request $request) {
if ($request->expectsJson()) {
return response()->json([
'type' => 'https://example.com/errors/validation',
'title' => 'Validation Failed',
'status' => 422,
'errors' => $e->errors(),
], 422);
}
});
$this->renderable(function (ModelNotFoundException $e, Request $request) {
if ($request->expectsJson()) {
return response()->json([
'type' => 'https://example.com/errors/not-found',
'title' => 'Resource Not Found',
'status' => 404,
], 404);
}
});
}
Conclusion
A scalable API is not just code that runs — it is an architecture that lets your team grow without accumulating technical debt. Repository Pattern, versioning, queues, and caching are the four pillars every Laravel production API needs. Ventra Rocket applies these patterns consistently across all backend projects.