Laravel: creating custom Response to ModelNotFoundException or customizing Missing Model Behavior

Alexey Shatrov
3 min readFeb 4, 2024

--

While developing an application using Laravel Implicit Binding you can face a problem, when customizing “missing model” logic using documentation is not very suitable when you have lots of routes. Remind, that the handler looks like this

Route::get('/users/{user}', [UserController::class, 'view'])
->missing(function (Request $request) {

//Any your logic here, for example return JSON message

return response()->json([
'message' => 'User not found.'
], 404);

});

Now imagine, that you need to do this for every route… Let’s deal with this!

Option 1. Put the handler function into the variable

This solution is simple and works well when you do not have many callbacks

$missingUserHandler = function (Request $request) {
return response()->json([
'message' => 'User not found.'
], 404);
};

Route::get('/users/{user}', [UserController::class, 'view'])->missing($missingUserHandler);

But when you have lots of handlers, the routes file becomes too cluttered, so I prefer another method

Option 2. Set ModelNotFoundException logic in the Model using Explicit Binding

This method is more efficient than the previous one because all logic moves into the RouteServiceProvider. Additionally, it will be applied to all routes using this binding.

Define an explicit binding for the model and pass a callback as a third parameter that is called when the model is not found.

public function boot()
{
parent::boot();

Route::model('user', \App\Models\User::class, function ($user) {
return response()->json([
'message' => 'User not found.'
], 404);
});
}

This solution makes sense when you don’t have many models and use an explicit binding. And, of course, there is no sense in writing extra code to implement explicit binding when implicit binding works well :)

Option 3. Catch ModelNotFoundException in app/Exceptions/Handler.php

You can implement a custom rendering closure for exceptions of a given type, using a renderable method of your exception handler as described in the documentation. Sounds like a solution, but not so fast — this code in app/Exceptions/Handler.php will do nothing.

$this->renderable(function (ModelNotFoundException $e, $request) {
if ($request->is('api/*')) {
return response()->json([
'message' => 'Record not found.'
], 404);
}
});

The reason is simple — ModelNotFoundException is not passed directly to Handler.php; instead, it is transformed into a NotFoundHttpException.

Also, simply replacing ModelNotFoundException with NotFoundHttpException is not a good option, as this code will handle all NotFoundHttpExceptions, not just those for models

$this->renderable(function (NotFoundHttpException $e, $request) {
if ($request->is('api/*')) {
return response()->json([
'message' => 'Record not found.'
], 404);
}
});

If you implement the code above - querying any non-existing URI starting with api will result in a confusing message stating that the record is not found ^_^.

To fix this, it is necessary to add a check that the previous exception is a ModelNotFoundException. Additionally, you can implement a corresponding response for each Model class. Here is an example:

$this->renderable(function (NotFoundHttpException $e) {
if (request()->is('api/*') && ($e->getPrevious() instanceof \Illuminate\Database\Eloquent\ModelNotFoundException)) {
$message = match ($e->getPrevious()->getModel()) {
'App\Models\User' => 'User not found.'

//... Add Other models here

};
return response()->json(['message' => $message]);
}
});

The code above works well, but if your messages differ only by model names, you can add a bit of magic and automatically extract model names from the passed model. Here is the final version:

$this->renderable(function (NotFoundHttpException $e) {
if (request()->is('api/*') && ($e->getPrevious() instanceof \Illuminate\Database\Eloquent\ModelNotFoundException)) {
$model = Str::afterLast($e->getPrevious()->getModel(), '\\'); //extract Model name
return response()->json(['message' => $model.' not found', 404]);
}
});

Afterword

When building an API, I prefer an approach where all responses are handled by an ApiResponse class, as described in this Medium article. The first step is to add a method to this class that handles all 404 Not Found errors:

class TheOneResponse
{
....

public static function notFound(string $errorMessage = "Item not found")
{
return new static(404, errorMessage: $errorMessage)
}
}

Then, you can call this method in a handler like:

return TheOneResponse::notFound($model.' not found');

This approach is clean and easily customizable, allowing you to change the response format with minimal effort.

Also there is a good practice is to use Symfony\Component\HttpFoundation\Response class to define HTTP statuses instead of hardcoding values, for example in this article all 404 can be replaced with Response::HTTP_NOT_FOUND

That’s it!

Use any option you like, write any questions to comments and happy coding!

--

--