https://laraveldaily.com/post/bad-practices-laravel-api
6 Bad Practices When Building Laravel APIs
Laravel allows us to structure code in many ways, right? But with APIs, it's important to avoid some bad practices, cause it may break API clients and confuse other developers.
1. Returning Status Code 200 When Errors Occur
One of the most common mistakes is returning a 200 OK status code when something has actually gone wrong.
Bad Practice:
public function store(Request $request)
{
try {
if (!$request->has('name')) {
return response()->json([
'success' => false,
'error_message' => 'Name is required'
], 200); // WRONG! Using 200 for an error
}
// ...
} catch (\Exception $e) {
return response()->json([
'success' => false,
'error_message' => 'Something went wrong'
], 200); // WRONG again!
}
}
Better Approach:
public function store(Request $request)
{
try {
$validated = $request->validate([
'name' => 'required|string|max:255',
]);
$user = User::create($validated);
return response()->json([
'data' => $user
], 201); // Good 201 status code for success
} catch (ValidationException $e) {
return response()->json([
'message' => 'Validation failed',
'errors' => $e->errors()
], 422); // Not 200 anymore!
} catch (\Exception $e) {
return response()->json([
'error_message' => 'Server error'
], 500); // Also not 200!
}
}
There's even a classical meme about it, found on Reddit:
And it's not just about error/success status code. As you can see in the example, we're returning 4xx code for validation, and 5xx code for general Exception on the server.
So, use appropriate HTTP status codes - 201 for creation, 422 for validation errors, 404 for not found, etc. This helps API clients properly understand the response.
2. Not Following RESTful Conventions
For REST APIs, developers typically use Resource Controllers in Laravel. Some different non-standard naming and HTTP methods can make your API confusing and hard to maintain.
Bad Practice:
// Routes file
Route::get('/getUserById/{id}', [UserController::class, 'getOneUser']);
Route::post('/createUser', [UserController::class, 'makeUser']);
Route::post('/deleteUser/{id}', [UserController::class, 'removeUser']);
Route::get('/getAllUsers', [UserController::class, 'fetchAllUsers']);
Better Approach:
Use Laravel's resource Controllers to enforce RESTful conventions:
// Routes file
Route::apiResource('users', UserController::class);
// UserController.php
class UserController extends Controller
{
// GET /users
public function index()
{
// ...
}
// POST /users
public function store(Request $request)
{
// ...
}
// GET /users/{id}
public function show(User $user)
{
// ...
}
// PUT/PATCH /users/{id}
public function update(Request $request, User $user)
{
// ...
}
// DELETE /users/{id}
public function destroy(User $user)
{
// ...
}
}
This automatically maps HTTP verbs to CRUD actions and follows conventional REST naming patterns.
3. Making Breaking API Changes Without Versioning
If you have real API clients already using your API, changing response structures or endpoint behaviors can break them.
Bad Practice:
// BEFORE: Client expects 'name' field
public function show($id)
{
$user = User::findOrFail($id);
return [
'id' => $user->id,
'name' => $user->name,
'email' => $user->email
];
}
// AFTER: Breaking change! 'name' split into 'first_name' and 'last_name'
public function show($id)
{
$user = User::findOrFail($id);
return [
'id' => $user->id,
'first_name' => $user->first_name,
'last_name' => $user->last_name,
'email' => $user->email
];
}
Better Approach - Option 1:
Use API versioning:
// routes/api.php
Route::prefix('v1')->group(function () {
Route::apiResource('users', 'Api\V1\UserController');
});
Route::prefix('v2')->group(function () {
Route::apiResource('users', 'Api\V2\UserController');
});
// App\Http\Controllers\Api\V1\UserController.php - Original structure
// App\Http\Controllers\Api\V2\UserController.php - New structure
If you want to find out more about API versioning, we have a long tutorial about it.
Better Approach - Option 2:
Or maintain backward compatibility:
public function show($id)
{
$user = User::findOrFail($id);
return [
'id' => $user->id,
'name' => $user->first_name . ' ' . $user->last_name, // Keep for compatibility
'first_name' => $user->first_name,
'last_name' => $user->last_name,
'email' => $user->email
];
}
4. NOT Using API Resources ("Reinventing the Wheel")
Many developers create custom response formatters when Laravel already offers API Resources.
Bad Practice:
public function index()
{
$users = User::all();
$response = [];
foreach ($users as $user) {
$response[] = [
'id' => $user->id,
'full_name' => $user->name,
'email_address' => $user->email,
// ... Manual transformation
];
}
return response()->json(['data' => $response]);
}
Better Approach:
Use Laravel's API Resources:
// Create with: php artisan make:resource UserResource
class UserResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'joined_date' => $this->created_at->format('Y-m-d'),
'is_admin' => $this->hasRole('admin'),
];
}
}
// Then in your controller:
public function index()
{
return UserResource::collection(User::all());
}
public function show(User $user)
{
return new UserResource($user);
}
API resources provide not only consistent structure, but also pagination support, and better maintainability.
There's even a saying "don't fight the framework". Of course, there are exceptions, but you have to have a good reason to creating something custom here.
5. Inconsistent Error Response Structure
We already talked about API status code for success/errors, now let's talk about error messages.
Having different error formats across your API endpoints makes client error handling difficult.
Bad Practice:
// In one controller:
return response()->json(['error' => 'User not found'], 404);
// In another controller:
return response()->json(
['status' => 'fail', 'message' => 'Not found'], 404);
// In a third controller:
return response()->json(
['code' => 404, 'details' => 'The user does not exist'], 404);
Better Approach:
Create a consistent error handling trait:
// App\Traits\ApiResponder.php
trait ApiResponder
{
protected function success($data, $code = 200)
{
return response()->json(['data' => $data], $code);
}
protected function error($message, $code)
{
return response()->json([
'error' => [
'message' => $message,
'code' => $code
]
], $code);
}
}
// Then in your controller:
use App\Traits\ApiResponder;
class UserController extends Controller
{
use ApiResponder;
public function show($id)
{
$user = User::find($id);
if (!$user) {
return $this->error('User not found', 404);
}
return $this->success(new UserResource($user));
}
}
6. Missing Rate Limiting
APIs without rate limiting are vulnerable to abuse, whether intentional (like a DDoS attack) or accidental (just poorly written scraper).
Use Laravel's built-in rate limiting:
// app/Providers/AppServiceProvider.php
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
protected function boot(): void
{
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
}
Conclusion
All in all, remember that Laravel provides many tools to help implement these best practices - use them rather than fighting the framework's conventions.