In modern web development, Laravel is widely used for building APIs due to its elegant syntax and powerful features rather than just using plain blade templates. Today it powers different frontend applications, including mobile apps and single-page applications (SPAs). Understanding how to design APIs that are efficient and maintainable is crucial for any developer working with Laravel. This guide will walk you through the essential principles and best practices for designing APIs that don’t suck in Laravel.
Clean Resourse Naming #
A common mistake in API design is using inconsistent or unclear resource names. REST is resource-oriented, so it’s essential to use nouns that represent the resources clearly. The HTTP methods (GET, POST, PUT, DELETE) should be used to perform actions on these resources.
Route::get('/tasks', [TaskController::class, 'index']); // Get all tasks
Route::post('/tasks', [TaskController::class, 'store']); // Create a new task
Route::get('/tasks/{id}', [TaskController::class, 'show']); // Get a specific task
Route::put('/tasks/{id}', [TaskController::class, 'update']); // Update a specific task
Route::delete('/tasks/{id}', [TaskController::class, 'destroy']); // Delete a specific task
Use Resource Classes #
Laravel provides resource classes to transform your models into JSON responses. This helps in maintaining a consistent structure for your API responses and makes it easier to manage changes in the future. Instead of returning Eloquent models like return Task::all();, use resource classes.
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class TaskResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'description' => $this->description,
'completed' => (bool) $this->completed,
'completed_at' => $this->completed_at?->toISOString(),
'created_at' => $this->created_at->toISOString(),
'updated_at' => $this->updated_at->toISOString(),
];
}
}
Then, in your controller, you can return the resource like this:
public function index()
{
return TaskResource::collection(
Task::all()
);
}
public function show(Task $task)
{
return new TaskResource($task);
}
Use Standard Methods #
Don’t reinvent the wheel. Stick to standard HTTP methods for CRUD operations. This makes your API predictable and easier to understand for other developers.
As an example, GET /tasks/delete?id=123 should never be used to delete a resource. Instead, use DELETE /tasks/123.
Return Proper HTTP Status Codes #
Using appropriate HTTP status codes is vital for communicating the result of an API request. The API clients need to programmatically understand the outcome of their requests without parsing the response body..
// Creating a resource
return (new TaskResource($task))
->response()
->setStatusCode(201);
// Successful deletion
return response()->json(null, 204);
// Not found (handled automatically with route model binding)
return response()->json([
'message' => 'Task not found'
], 404);
Get comfortable with the most common status codes such as 200 (OK), 201 (Created), 204 (No Content), 400 (Bad Request), 404 (Not Found), and 422 (Unprocessable Entity). They are pretty common in API development.
Validate Input Data #
Always validate incoming data to ensure that your API receives the expected format and values. User Laravel’s Form Request classes to handle validation cleanly.
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreTaskRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'title' => ['required', 'string', 'max:255'],
'description' => ['nullable', 'string'],
'completed' => ['boolean'],
];
}
}
Then, use this request class in your controller:
public function store(StoreTaskRequest $request)
{
$task = Task::create($request->validated());
return new TaskResource($task);
}
Implement Pagination #
When dealing with large datasets, it’s essential to implement pagination to avoid overwhelming the client and server. Laravel makes it easy to paginate results using the paginate method.
public function index(Request $request)
{
$query = Post::with(['category', 'author']);
return PostResource::collection(
$query->latest()->paginate(15)
);
}
Laravel’s pagination automatically includes metadata such as total items, items per page, current page, and links to the next and previous pages. These data can be used by the client to navigate through paginated results.
Filtering and Sorting #
APIs often need to support filtering and sorting of resources. You can achieve this by accepting query parameters in your requests.
public function index(Request $request)
{
$query = Task::query();
if ($request->has('completed')) {
$query->where('completed', $request->boolean('completed'));
}
if ($request->has('sort_by')) {
$query->orderBy($request->input('sort_by'), $request->input('sort_order', 'asc'));
}
return TaskResource::collection(
$query->paginate(15)
);
}
Version Your API #
As your API evolves, it’s crucial to version it to avoid breaking changes for existing clients. A common approach is to include the version number in the URL.
Route::prefix('v1')->group(function () {
// Version 1 routes
});
Route::prefix('v2')->group(function () {
// Version 2 routes with breaking changes
});
Authentication #
Secure your API using Laravel’s built-in authentication mechanisms like Sanctum or Passport. Ensure that sensitive endpoints are protected and only accessible to authenticated users. Below is an example of using Sanctum for API authentication.
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens, Notifiable;
// ...
}
Then, protect your routes using the auth:sanctum middleware:
Route::middleware('auth:sanctum')->group(function () {
Route::get('/user', [UserController::class, 'show']);
});
Rate Limiting #
To prevent abuse and ensure fair usage of your API, implement rate limiting using Laravel’s built-in features. You can define rate limits in your RouteServiceProvider or directly in your routes.
RateLimiter::for('api', function (Request $request) {
$apiKey = ApiKey::where('key', $request->bearerToken())->first();
if (!$apiKey) {
return Limit::perMinute(10); // Unauthenticated: 10/min
}
return match ($apiKey->tier) {
'free' => Limit::perMinute(60)->by($apiKey->id),
'pro' => Limit::perMinute(300)->by($apiKey->id),
'enterprise' => Limit::none(),
};
});
Below is how you can apply the rate limiter to your API routes:
Route::middleware(['throttle:api'])->group(function () {
Route::get('/tasks', [TaskController::class, 'index']);
});
Laravel returns a 429 Too Many Requests response when the rate limit is exceeded.
N+1 Problem Prevention #
When designing APIs that interact with databases, it’s crucial to address the N+1 query problem, which can lead to performance issues. Laravel’s Eloquent ORM provides a straightforward way to prevent this issue using eager loading.
Below is an example of how this issue is typically encountered and how to resolve it:
// N+1 Problem Example
public function index()
{
$posts = Post::all(); // This will execute one query to get all posts
foreach ($posts as $post) {
echo $post->author->name; // This will execute an additional query for each post to get the author
}
}
To prevent the N+1 problem, you can use the with method to eager load the related models:
// Eager Loading Example
public function index()
{
$posts = Post::with('author')->get(); // This will execute two queries:
foreach ($posts as $post) {
echo $post->author->name; // No additional queries will be executed here
}
}
In the first example, if there are 100 posts, it would result in 101 queries (1 for posts + 100 for authors). In the second example, it only executes 2 queries regardless of the number of posts, significantly improving performance.
Documentation #
This step is often overlooked but is crucial for a successful API. Use tools like Swagger or Scribe to create comprehensive documentation for your API. Scribe can automatically generate documentation from your Laravel routes and controllers, Add the following to your controller methods to enhance the generated documentation:
/**
* Display a listing of the tasks.
* @return \Illuminate\Http\Response
* @group Tasks
* @response 200 {
* "data": [
* {
* "id": 1,
* "title": "Sample Task",
* "description": "This is a sample task description.",
* "completed": false,
* "created_at": "2025-12-05T12:00:00Z",
* "updated_at": "2025-12-05T12:00:00Z"
* }
* ]
* }
*/
public function index()
{
return TaskResource::collection(
Task::all()
);
}
Scribe will use these annotations to generate detailed API documentation, making it easier for developers to understand how to interact with your API.