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.

Anything we've missed in this list?

avatar

I would probably add caching as well - making lots of the same calls can be reduced with caching especially good if the API has some form of rate limiting

👍 1
avatar

Yeah good point, but it's not a bad practice I would say, it's more like an improvement. Also, properly using cache is not a trivial skill.

avatar
You can use Markdown
avatar

What about race condition? It's very important for a server to prevent multiple calls, especially when the system operates with money or some other sensitive stuff.

avatar

Yeah, also important but more like "advanced" skill, I would say, for larger or, as you're saying, more sensitive projects.

avatar

But is it really advanced? It's just a protection from multiple clicks on a website when debouncing is off

avatar

Advanced in terms of how to implement it (and, more importantly, how to TEST it) properly. It's not "just" a protection, like validation rule or something similar.

avatar
You can use Markdown
avatar

I have a question about the resource routes. I had them , but then i added something with relationships and had to add a route for attach/dettach. So what would be the best practis for adding this routes ?

avatar

The non-resource custom routes ON TOP of resource are usually personal preference. There's no "best practice" here.

avatar
You can use Markdown
avatar
You can use Markdown

Recent New Courses