Three years ago, I joined a startup that used Laravel for its backend. My first ticket: "add an endpoint to fetch a user's orders." Simple, right? Two days later, the API was in production... and completely broken. N+1 queries everywhere, no authentication on routes, inconsistent JSON responses depending on the endpoint. A silent disaster.
Since then, I've built dozens of APIs with Laravel. I've learned from my mistakes, often painfully. Here's the guide I wish I'd had back then.
Why Laravel is Perfect for REST APIs
Laravel isn't just a web framework. It's a complete platform that natively includes everything you need for a professional, maintainable API:
- Sanctum — token-based authentication without complex configuration
- API Resources — transform your Eloquent models into clean JSON
- Form Requests — centralized validation outside of controllers
- Rate Limiting — built-in protection in route definitions
- Queues & Events — async processing to avoid blocking responses
Project Structure: Think Scalability From Day One
The first thing to do before writing a line of code: version your API. Even if you only have one version today, you'll be grateful in six months when you need to introduce breaking changes without breaking existing clients.
app/
├── Http/
│ ├── Controllers/Api/V1/
│ │ ├── AuthController.php
│ │ ├── UserController.php
│ │ └── PostController.php
│ ├── Requests/Api/V1/
│ │ ├── LoginRequest.php
│ │ └── StorePostRequest.php
│ └── Resources/Api/V1/
│ ├── UserResource.php
│ └── PostResource.php
Authentication with Laravel Sanctum
Sanctum is the official solution for securing Laravel APIs. No manual JWT configuration, no complicated third-party libraries. Install it in three commands:
composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate
Here's a complete authentication controller with error handling:
class AuthController extends Controller
{
public function login(LoginRequest $request): JsonResponse
{
$user = User::where('email', $request->email)->first();
if (!$user || !Hash::check($request->password, $user->password)) {
return response()->json(['message' => 'Invalid credentials.'], 401);
}
$token = $user->createToken('api-token', ['*'], now()->addDays(30))->plainTextToken;
return response()->json([
'token' => $token,
'expires' => now()->addDays(30)->toIso8601String(),
'user' => new UserResource($user),
]);
}
public function logout(): JsonResponse
{
auth()->user()->currentAccessToken()->delete();
return response()->json(['message' => 'Successfully logged out.']);
}
}
API Resources: Only Expose What's Needed
This is probably the most underused feature by junior developers — and the most important. An API Resource controls exactly what you send to the client, preventing accidentally exposing sensitive fields:
class UserResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'role' => $this->role,
'avatar_url' => $this->avatar_url,
'created_at' => $this->created_at->toIso8601String(),
// Never: 'password', 'remember_token'...
];
}
}
Validation with Form Requests: Get It Out of Controllers
Absolute rule: never validate directly inside controllers. Your controller should do one thing — orchestrate the response. Validation belongs in a dedicated Form Request:
class StorePostRequest extends FormRequest
{
public function rules(): array
{
return [
'title' => ['required', 'string', 'max:255'],
'content' => ['required', 'string', 'min:100'],
'category_id' => ['required', 'exists:categories,id'],
'tags' => ['nullable', 'array'],
'tags.*' => ['exists:tags,id'],
];
}
}
Error Handling: Consistency Above All
A good API returns structured, predictable errors. Your clients — whether a mobile app or React frontend — need to handle errors uniformly. In bootstrap/app.php (Laravel 11):
->withExceptions(function (Exceptions $exceptions) {
$exceptions->render(function (ModelNotFoundException $e, Request $request) {
if ($request->is('api/*')) {
return response()->json(['message' => 'Resource not found.', 'error' => 'not_found'], 404);
}
});
})
Eliminating N+1 Queries with Eager Loading
One of the most frequent mistakes in Laravel APIs: fetching collections without pre-loading relationships. The result: 1 query for the list + 1 query per item to load the relation = N+1 queries.
// ❌ N+1 queries: production disaster
$posts = Post::all();
foreach ($posts as $post) {
echo $post->author->name; // 1 query PER post
}
// ✅ Eager loading: always just 2 queries
$posts = Post::with(['author', 'category', 'tags'])->paginate(20);
Conclusion: The 3 Golden Rules of a Good API
- Version from day one — trivial to add at the start, painful to retrofit later.
- Consistent responses — same JSON structure for all successes, same structure for all errors.
- Auto-document — use Scramble or L5-Swagger. An undocumented API is an unusable API.
The API you build today, others will use tomorrow. Make their life easy — including your future self.