Courses

How to Build Laravel 12 API From Scratch

Why 404 Page? Setting Correct Headers

Summary of this lesson:
- Handling API 404 errors correctly
- Seeing different ways for setting proper API headers
- Customizing API error messages for better security

In this lesson, we will set one correct header value to avoid returning web pages instead of JSON results.


As you can see in the /api/categories/1, we see data for one category.

But what happens if we pass a missing category? We get an HTML page.

But for API, it is wrong. API expects the result as a JSON. In your client, set the headers to Accept -> application/json. Now Laravel knows to send the result as a JSON.

Another way is to set headers using Middleware. First, let's create a Middleware.

php artisan make:middleware AlwaysAcceptJson

Inside Middleware, we have a Request. Middleware is not just for preventing something; you can also add something to that request and pass that to the next request.

app/Http/Middleware/AlwaysAcceptJson.php:

class AlwaysAcceptJson
{
public function handle(Request $request, Closure $next): Response
{
$request->headers->set('Accept', 'application/json');
 
return $next($request);
}
}

Now, we must register Middleware. Middleware can be registered to every Route or prepended to group web or api.

bootstrap/app.php:

return Application::configure(basePath: dirname(__DIR__))
->withProviders()
->withRouting(
web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware) {
$middleware->prepend(\App\Http\Middleware\AlwaysAcceptJson::class);
})
->withExceptions(function (Exceptions $exceptions) {
//
})->create();

As you can see, with the disabled header in the client, we still get the same error in JSON format.

When working with APIs, remember to set headers.


Customize Error Message

When the error message is received, in this example, we have a message for the "Model not found".

No query results for model [App\\Models\\Category] 13

The [App\\Models\\Category] part could be a security risk because a potential hacker could guess this is a Laravel project. Let's overwrite this message.

Exception can be added in the bootstrap/app.php file withExceptions method.

We need to render when the Exception is NotFoundHttpException. We can check the error message exception key to find which Exception to use.

We can return the custom message with a 404 status.

bootstrap/app.php:

use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
 
return Application::configure(basePath: dirname(__DIR__))
->withProviders()
->withRouting(
web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware) {
$middleware->prependToGroup('api', \App\Http\Middleware\AlwaysAcceptJson::class);
})
->withExceptions(function (Exceptions $exceptions) {
$exceptions->renderable(function (NotFoundHttpException $e) {
return response()->json(['message' => 'Object not found'], 404);
});
})->create();

We can see our custom message.

This way, if your application also has a web version, then in the browser, users will see the same message instead of an HTML 404 error. To prevent that from happening, we must make a simple check.

bootstrap/app.php:

use Illuminate\Http\Request;
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) {
$middleware->prependToGroup('api', \App\Http\Middleware\AlwaysAcceptJson::class);
})
->withExceptions(function (Exceptions $exceptions) {
$exceptions->renderable(function (NotFoundHttpException $e, Request $request) {
if ($request->wantsJson()) {
return response()->json(['message' => 'Object not found'], 404);
}
});
})->create();

Request is a second parameter for a renderable callback function.

Previous: Getting Single Record and API Resources
avatar

It may be worth adding the reason why the reportable method needs to be changed by the renderable one. In Laravel the report method is for logging or reporting exceptions, while the render method is for customizing the response sent back to the user when an exception occurs. By separating these concerns, Laravel allows you to handle exceptions in a flexible and organized manner.

👍 3
avatar

return response()->json(['message' => env('APP_DEBUG')? $e->getMessage() : 'Object not found'], 404);

Just use this

avatar

@donilearn Thanks for a good snippet.

    $exceptions->renderable(function (NotFoundHttpException $e, Request $request) {
        if ($request->wantsJson() && !config('app.debug')) {
            return response()->json(['message' => 'Object not found'], 404);
        }
    });

I think this code is little better. It achieves accessing env from config and getting full error stack.

avatar
You can use Markdown
avatar

$middleware->prependToGroup('api', [here the class is not launching])

avatar
Briere Mostafa Amine

Yes, me to

avatar
You can use Markdown
avatar

But we set middleware to always set headers application/json so if ($request->wantsJson()) kinda loses its purpose and if we remove middleware and user wont set header during api request , he will still get html... im a beginner and little bit confused.. but ill figure out something

👍 1
avatar
You can use Markdown
avatar

Good lesson. There is a typo also: prepented

avatar
You can use Markdown
avatar

n Laravel 12, when using $middleware->prepend('api', \App\Http\Middleware\AlwaysAcceptJson::class);, I get an HTML error: Target class [api] does not exist. I had to remove the first parameter from the prepend function to get a response for my HTTP request.

avatar

Replace $middleware->prepend('api', \App\Http\Middleware\AlwaysAcceptJson::class); by $middleware->prepend(\App\Http\Middleware\AlwaysAcceptJson::class);

avatar
You can use Markdown
avatar

For me, I normally use 'unauthorized' when in production and leave to not found in development

  • ->withExceptions(function (Exceptions $exceptions): void { $isProduction = app()->isProduction(); $exceptions->render(function (Throwable $e) use ($isProduction) { if ($e instanceof \Illuminate\Database\Eloquent\ModelNotFoundException) { $statusCode = $isProduction ? 401 : 404;

             return response()->json([
                 'message' => $isProduction ? 'Unauthorized' : 'Resource not found.',
                 'error' => $isProduction ? 'Unauthorized' : $e->getMessage(),
             ], $statusCode);
         }
    
avatar
You can use Markdown
avatar
You can use Markdown