Building Scalable REST APIs with Laravel: Architecture, Auth & Performance
Learn to architect, authenticate, and optimise REST APIs in Laravel that handle millions of requests without breaking. Real-world patterns from production API systems.
Every modern application needs an API. Your mobile app needs data. Your frontend needs endpoints. Third-party integrations need access to your system.
Building an API that works is straightforward. Building one that scales to millions of requests without breaking is hard.
We've built APIs serving millions of requests daily. We've scaled systems from hundreds to millions of users. We've debugged performance issues at 3 AM when traffic spiked unexpectedly.
Here's what we've learned about building APIs that don't fail when they succeed.
Why Laravel for APIs?
Laravel isn't the only PHP framework for APIs, but it's one of the best.
What Laravel provides out of the box:
- Built-in API authentication via Sanctum and Passport
- API Resources for consistent, controlled response transformation
- Rate limiting with fine-grained configuration
- Form Request classes for clean, reusable validation
- Eloquent ORM with eager loading for N+1 prevention
- Job queues for deferred processing
- Caching layers throughout (Redis, Memcached)
- A testing toolkit that makes writing API tests genuinely pleasant
RESTful Design Principles
Design around resources, not actions.
Good:
GET /api/v1/posts # List posts
GET /api/v1/posts/123 # Get a specific post
POST /api/v1/posts # Create a post
PUT /api/v1/posts/123 # Replace a post
PATCH /api/v1/posts/123 # Partially update a post
DELETE /api/v1/posts/123 # Delete a post
Avoid verb-based routes like /api/getPosts. Resources and HTTP verbs express intent clearly.
API Versioning
Version your API from the start.
/api/v1/posts # Current stable version
/api/v2/posts # Next version with breaking changes
Route prefix versioning in Laravel:
Route::prefix('v1')->group(function () {
Route::apiResource('posts', PostController::class);
});
Never break existing API consumers. Add a new version for breaking changes with documented deprecation timelines.
API Resources: Consistent Response Structure
Never return raw Eloquent models. Use API Resources to control what's exposed.
class PostResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'excerpt' => $this->excerpt,
'published_at' => $this->published_at?->toISOString(),
'author' => new UserResource($this->whenLoaded('author')),
];
}
}
Resources give you: control over exposed fields (never accidentally leak sensitive data), consistent date formatting, conditional relationship inclusion, and transformation logic isolated from controllers.
Authentication with Sanctum
Sanctum handles two scenarios:
SPA Authentication: Session-based authentication for your single-page application. CSRF protection included. No token management on the frontend.
Mobile/API Tokens: Personal access tokens with specific abilities (scopes) that can be revoked individually.
Route::middleware('auth:sanctum')->group(function () {
Route::get('/user', fn(Request $request) => $request->user());
Route::apiResource('posts', PostController::class);
});
Token Scopes:
$token = $user->createToken('mobile-app', ['posts:read', 'posts:write']);
Issue read-only tokens for integrations that don't need write access. Reduces the blast radius of a compromised token.
Performance Optimisation
Eliminate N+1 Queries
// BAD — 1 query for posts + N queries for authors
$posts = Post::all();
foreach ($posts as $post) {
echo $post->author->name; // Separate query per post
}
// GOOD — 2 queries total regardless of result size
$posts = Post::with('author')->get();
Eager load all relationships you'll access. Use Laravel Telescope to spot N+1 issues during development.
Always Paginate
Never return unbounded collections:
public function index(Request $request): AnonymousResourceCollection
{
$posts = Post::with(['author', 'categories'])
->published()
->latest()
->paginate(20);
return PostResource::collection($posts);
}
Cache Expensive Operations
$data = Cache::remember('api:posts:featured', 300, function () {
return Post::with('categories')->featured()->published()->get();
});
For user-specific data, include the user ID in the cache key. For public data, a shared cache with a reasonable TTL significantly reduces database load.
Rate Limiting
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
Return Retry-After headers on 429 responses so consumers know when to retry.
Consistent Error Handling
// bootstrap/app.php
->withExceptions(function (Exceptions $exceptions) {
$exceptions->render(function (ModelNotFoundException $e, Request $request) {
if ($request->expectsJson()) {
return response()->json([
'message' => 'Resource not found.',
'error' => 'not_found',
], 404);
}
});
})
Standard HTTP status codes, consistent JSON structure, human-readable messages. Document every error code.
Testing Your API
APIs are contracts. Test them as contracts.
public function test_authenticated_user_can_list_posts(): void
{
$user = User::factory()->create();
Post::factory()->count(5)->published()->create();
$this->actingAs($user)
->getJson('/api/v1/posts')
->assertOk()
->assertJsonCount(5, 'data')
->assertJsonStructure([
'data' => [['id', 'title', 'published_at']],
'meta' => ['current_page', 'total'],
]);
}
Test the happy path, authentication failures, validation errors, and edge cases. An API without tests is an API that will break in production.
Need Help Building Your API?
Contact our development team to discuss your project. We've built APIs for startups and enterprises — we know what scales.
Skyline Softech builds scalable Laravel APIs for web and mobile applications for businesses across the UK, Australia, and New Zealand. Learn more about our web development services.