How to enforce JSON format in API Requests and Responses

How to enforce JSON format in API Requests and Responses

Best Practices for Structuring, Validating, and Enforcing JSON in Laravel API Development

Introduction

There are several ways to ensure standard JSON format in API requests and responses which enforces consistent and structured data exchange. In this article, we’ll explore how to enforce JSON format on incoming requests in a Laravel application and outgoing responses.

How to enforce Request in JSON Format

When designing APIs in Laravel, it is important to enforce requests to be in JSON format. This helps ensure consistency, prevents unexpected request formats (such as HTML or other types other than JSON), and avoids receiving data from random or unintended sources. Additionally, enforcing JSON format improves security and simplifies request handling.

Here’s out to enforce API request to be in JSON format in Laravel.

Create a middleware EnforceJsonRequest

php artisan make:middleware EnforceJsonRequest
💡
You’ll find this video on my Youtube channel resourceful if you’re new to Laravel middleware:

The EnforceJsonRequest middleware is located in app/Http/Middleware/EnforceJsonRequest.php

Define EnforceJsonRequest Middleware Condition

  • Open the EnforceJsonRequest.php and define the middleware conditions to intercept requests and check the format
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class EnforceJsonRequest
{
    /**
     * Handle an incoming request.
     *
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     */
    public function handle(Request $request, Closure $next): Response
    {
        $contentType = $request->header('Content-Type');

        if (strpos($contentType, 'application/json') === false && strpos($contentType, 'multipart/form-data') === false) {
            return response()->json(['error' => 'Invalid Content-Type. Must be JSON or multipart/form-data.'], Response::HTTP_EXPECTATION_FAILED);
        }

        return $next($request);
    }
}

The code validates the request's Content-Type header to ensure it is either application/json or multipart/form-data (commonly used for file uploads in APIs). If the request does not meet this requirement, it returns an error response, preventing invalid or unexpected data formats.

Register the EnforceJsonRequest middleware

  • Register the middleware in the withMiddleware callback function inbootstrap/app.php
     ->withMiddleware(function (Middleware $middleware) {
        $middleware->trustProxies(at: '*');
        $middleware->alias([
            'enforce-json-request' => EnforceJsonRequest::class,
        ]);
    })

Set EnforceJsonRequest Middleware to Routes

Apply the middleware to the API routes that must receive request in json format:

In a fresh Laravel application, the api.php setup is not included by default. However, you can generate it using the command php artisan install:api or manually create the api.php file and register it within the withRouting method in bootstrap/app.php.

    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        api: __DIR__.'/../routes/api.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
  • Create the api.php file and set the middleware on the routes
<?php

use Illuminate\Support\Facades\Route;

Route::prefix('/v1')->middleware(['enforce-json-request'])->group(function () {

    Route::get('/', function () {
        return response()->json([
            'message' => 'Request came in JSON Format',
        ], 200);
    });
});

Serve the application using the php artisan serve command:

💡
If you’re new to Laravel and would love to learn about the php artisan serve command

Incoming Request in Action

  • Request to the application without content-type header of application/json

  • Request with content-type set to application/json

How to enforce JSON response

Laravel checks the Accept header in the request to determine the appropriate response. However, browsers and Postman often send Accept: */*, so simply rewriting this header ensures Laravel handles the rest.

Create the EnforceJsonResponse Middleware

Use the command below to create a middleware with any name of your choice.

php artisan make:middleware EnforceJsonResponse

Define the EnforceJsonResponse Middleware condition

  • Update the EnforceJsonResponse middleware in the app/Http/Middleware/EnforceJsonResponse.php
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class EnforceJsonResponse
{
    /**
     * Handle an incoming request.
     *
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     */
    public function handle(Request $request, Closure $next): Response
    {

        $request->headers->set('Accept', 'application/json');

        $response = $next($request);

        if (!$response instanceof JsonResponse) {
            return response()->json($response->original ?? ['message' => 'Invalid response'], $response->status());
        }

        return $response;
    }
}

Register the middleware EnforceJsonResponse

Register the middleware in bootstrap/app.php

    ->withMiddleware(function (Middleware $middleware) {
        $middleware->trustProxies(at: '*');
        $middleware->alias([
            'enforce-json-request' => EnforceJsonRequest::class,
            'enforce-json-response' => EnforceJsonResponse::class,
        ]);
    })

Enforce Response Middleware on Routes

  • Define a route that returns a response not in json format
<?php

use Illuminate\Support\Facades\Route;

Route::prefix('/v1')->middleware(['enforce-json-request'])->group(function () {

    Route::get('/', function () {
        return 'Request came in JSON Format';
    });
});
  • Output is in HTML Format when request is made without enforce-json-response on the route returning response.

  • Add the enforce-json-response to the API route
<?php

use Illuminate\Support\Facades\Route;

Route::prefix('/v1')->middleware(['enforce-json-request', 'enforce-json-response'])->group(function () {

    Route::get('/', function () {
        return 'Request came in JSON Format';
    });
});
  • Or use appendToGroup on withMiddleware callback function in the bootstrap/app.php to keep the api route clean
    ->withMiddleware(function (Middleware $middleware) {
        $middleware->trustProxies(at: '*');
        $middleware->alias([
            // 'enforce-json-request' => EnforceJsonRequest::class,
            // 'enforce-json-response' => EnforceJsonResponse::class,
        ]);
        $middleware->appendToGroup('enforce-json', [
            EnforceJsonResponse::class,
            EnforceJsonRequest::class
        ]);
    })
  • API route
<?php

use Illuminate\Support\Facades\Route;

Route::prefix('/v1')->middleware(['enforce-json'])->group(function () {
    Route::get('/', function () {
        return 'Request came in JSON Format';
    });
});

Response in Action

  • Output in JSON format when request is made with enforce-json-response on the route returns response.

Handling Exceptions response in JSON format


    ->withExceptions(function (Exceptions $exceptions) {

        $exceptions->shouldRenderJsonWhen(function (Request $request, Throwable $th) {
            return $request->is('api/*') || $request->expectsJson();
        });

    })->create();
  • Returns error in JSON format but with too much technical details needed for debugging purposes only
{
    "message": "syntax error, unexpected token \"}\", expecting \";\"",
    "exception": "ParseError",
    "file": "/path to error file",
    "line": 13,
    "trace": [
    ]
}
  • You can define a global exception handling error response in JSON to return the message alone
    ->withExceptions(function (Exceptions $exceptions) {
        $exceptions->renderable(function (Throwable $e, Request $request) {
                return new JsonResponse([
                    'message' => $e->getMessage(),
                ], $e->getCode() ?: 500);
        });
    })

Handle individual exception response in JSON format

Alternatively, you can handle individual exceptions by returning custom responses in JSON format.

    ->withExceptions(function (Exceptions $exceptions) {

        $exceptions->shouldRenderJsonWhen(function (Request $request, Throwable $th) {
            return $request->is('api/*') || $request->expectsJson();
        });

        $exceptions->renderable(function (ParseError $e) {
            return response()->json([
                'message' => "Syntax error",
            ], Response::HTTP_INTERNAL_SERVER_ERROR);
        });


    })
  • Output

💡
Laravel provides several built-in exceptions, such as NotFoundHttpException, ParseError, MethodNotAllowedHttpException etc. You can customize how these errors are handled to make the error message clear and user friendly while logging technical details for debugging purpose.

Conclusion

You can define and structure request and responses in Laravel when building APIs in JSON format. It makes provision for consistency, clean code, proper data exchange format etc. In this article, we’ve explore best practices for enforcing JSON format in Laravel, including request validation, response formatting, and error handling to create well-structured and predictable APIs. Implementing these best practices enhances API performance and improves integration with frontend applications and third-party services. Laravel developers can build more robust and maintainable APIs, by prioritizing JSON enforcement.

Find this article useful… kindly share with your network and feel free to use the comment section for questions, answers, and contributions.

💡
Follow me on Hashnode: Alemsbaja --- X: Alemsbaja---- Youtube: Tech with Alemsbaja to stay updated on more articles

Did you find this article valuable?

Support Alemoh Rapheal Baja by becoming a sponsor. Any amount is appreciated!