Implicit fancy 404 json instead of ModelNotFoundException in laravel API

Updated: 23th April 2025
Tags: laravel

Note: this article for laravel 11, laravel 12. Other versions not tested.

For lazy artisans like me that use code like this:

<?php
$user = User::query()->findOrFail(1);

You will notice API is returning

{
"message": "No query results for model [App\\Models\\User] 1"
}

Well it is nice, but I want it to say User not found.

So we will redefine response for api that has ModelNotFoundException

Edit bootstrap/app.php file, find ->withExceptions(function (Exceptions $exceptions) { and make it like this

<?php
// bootstrap/app.php
// .....
->withExceptions(function (Exceptions $exceptions) {
        $exceptions->render(function (NotFoundHttpException $e, Request $request) {
            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);
            }
        });
....

Full bootstrap/app.php should look similar to this:

<?php

use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        api: __DIR__.'/../routes/api.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware): void {
        //
    })
    ->withExceptions(function (Exceptions $exceptions) {
        $exceptions->render(function (NotFoundHttpException $e, Request $request) {
            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);
            }
        });
    })->create();

See this line:

<?php
$model = Str::afterLast($e->getPrevious()->getModel(), '\\'); // extract Model name`.

This will grab default message and extract model name from it. So we will get User not found, Product not found, etc message implicitly like true artisans.

For example, you have model Item777. But you want users to see Fancy product. So you go fancy mode:

<?php
$model = Str::afterLast($e->getPrevious()->getModel(), '\\'); // extract Model name
$fancyNames = ['Item777' => 'Fancy product', 'User' => 'Customer'];
$fancyName = $fancyNames[$model] ?? $model;
return response()->json(['message' => $fancyName.' not found'], 404);

Or you can return generic response and keep it obscure:

<?php

return response()->json(['message' => 'Record not found. Go away now!'], 404);

The only thing you need to do is to write ->findOrFail($id) or if you are inside $user, $user->products()->firstOrFail()