Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Json Response Laravel Package

dragon-code/laravel-json-response

A Laravel package for building consistent JSON API responses. It provides fluent helpers for success, error, and custom payloads, helping standardize response structure and reduce boilerplate across controllers and APIs.

View on GitHub
Deep Wiki
Context7

Getting Started

What it actually does: the package ships a single middleware that rewrites the incoming request's Accept header to application/json before the rest of the stack sees it. That means $request->expectsJson() / wantsJson() return true everywhere, so Laravel's exception handler, validation, auth middleware, and abort() all take their JSON code paths — even when the client sends Accept: text/html or nothing at all. It is not a response transformer; it only changes what the framework believes the client wants.

Install

composer require dragon-code/laravel-json-response

Version note: 3.2.0 added Laravel 13 support, so ^3.2 is the constraint to use on a Laravel 13 app. On Laravel 10–12 any recent 3.x works. The package's whole surface area is still one middleware class, so upgrades carry essentially no migration cost — which is exactly why you should read the changelog rather than fear it.

Register the middleware — it must be prepended, not appended (see Gotchas for why).

Laravel 11/12/13 (bootstrap/app.php):

->withMiddleware(function (Middleware $middleware) {
    // whole app is JSON
    $middleware->prepend(\DragonCode\JsonResponse\Middleware\JsonResponse::class);

    // or: only the api group
    // $middleware->api(prepend: [\DragonCode\JsonResponse\Middleware\JsonResponse::class]);
})

Laravel 10 and below (app/Http/Kernel.php):

protected $middleware = [
    \DragonCode\JsonResponse\Middleware\JsonResponse::class,
    // ...
];

Where to look first: open the middleware file itself (it is a few lines) and then your bootstrap/app.php / App\Exceptions\Handler. Those two places determine 90% of the behaviour you'll observe.

First use case — the smoke test: with the middleware active, hit a route that doesn't exist using a client that sends no Accept header:

curl -i http://myapp.test/api/nope

Before: an HTML error page (or a redirect to the login page for auth middleware). After: {"message": "..."} with the correct status code and Content-Type: application/json. Do the same against a FormRequest that fails validation and confirm you get a 422 with an errors object.

Implementation Patterns

API-only project (the intended use). Prepend to the api group in bootstrap/app.php. Sanctum/Passport token auth then returns 401 JSON instead of attempting a redirect, and SubstituteBindings 404s render as JSON.

$middleware->api(prepend: [
    \DragonCode\JsonResponse\Middleware\JsonResponse::class,
]);

Mixed app (web + api in one codebase). Do not register globally. Scope it to the route group instead, so session/CSRF/login redirects keep working for your Blade users:

Route::middleware([\DragonCode\JsonResponse\Middleware\JsonResponse::class])
    ->prefix('api')
    ->group(base_path('routes/api.php'));

Validation without client cooperation. This is the biggest day-to-day win: consumers stop needing Accept: application/json to get a proper 422 instead of a redirect-with-errors-in-session. Combined with a JsonResource or a small response helper, you get a consistent envelope:

class StoreUserRequest extends FormRequest
{
    public function rules(): array
    {
        return ['email' => ['required', 'email', 'unique:users']];
    }
}
// POST without Accept header -> 422 {"message": "...", "errors": {"email": [...]}}

Error paths you no longer have to hand-write. With expectsJson() true, these all render as JSON automatically:

Route::get('/users/{user}', fn (User $user) => $user);  // missing -> 404 JSON
abort(403, 'Nope');                                     // -> 403 JSON
$this->authorize('view', $user);                        // -> 403 JSON
// ThrottleRequests, Authenticate, VerifyCsrfToken      // -> 429/401/419 JSON

Combine with Route::fallback(fn () => response()->json(['message' => 'Not Found'], 404)) so unmatched URLs inside your group are JSON too.

Consistent error shape. The package does not standardise your payloads — it only flips the negotiation flag. Use withExceptions() in bootstrap/app.php to normalise everything, and let the middleware guarantee that this hook is actually reached:

->withExceptions(function (Exceptions $exceptions) {
    $exceptions->render(function (Throwable $e, Request $request) {
        if ($request->expectsJson() && $e instanceof HttpExceptionInterface) {
            return response()->json([
                'message' => $e->getMessage(),
                'errors'  => [],
            ], $e->getStatusCode());
        }
    });
})

Testing pattern. Do not rely on the middleware in your feature tests — keep using $this->getJson() / $this->postJson() so the intent is explicit and the suite doesn't silently depend on a middleware registration. Reserve one or two tests that call plain $this->get() to assert the forcing behaviour itself (e.g. asserting a 422 comes back for a bare POST).

Integration notes. Pairs naturally with fruitcake/laravel-cors (now built in), Sanctum, and any SPA front end where the fetch client forgets headers. If you expose the same routes to browsers via axios, remember axios already sends Accept: application/json, so the package mostly buys you protection against other consumers: curl scripts, mobile clients, webhooks, and postman collections with the header stripped.

Gotchas and Tips

  • It does not convert your responses. return view('welcome') still returns HTML. The middleware only influences branches that consult expectsJson(). If you want a hard guarantee, add a terminating check in your own middleware or a withResponses assertion in tests.

  • Ordering is everything. Registered globally with append/at the bottom of $middleware, Authenticate and ThrottleRequests may already have run and taken the HTML path. Always prepend (or $middleware->prepend(...)), and place it before SubstituteBindings in the api group.

  • Login redirects disappear. On any route using auth, an unauthenticated browser user now gets 401 {"message":"Unauthenticated."} with no redirect to /login. That is correct for APIs and catastrophic for a Blade app — hence the route-group scoping above.

  • CSRF becomes 419 JSON. VerifyCsrfToken failures come back as a JSON 419 rather than a friendly page. Fine for APIs, confusing for humans poking a web route in a browser.

  • Your debug page goes away. Spatie Ignition / Whoops check expectsJson() and will hand you a JSON stack trace instead of the pretty error screen in local development. Guard it:

    if (! app()->isLocal()) {
        $middleware->prepend(\DragonCode\JsonResponse\Middleware\JsonResponse::class);
    }
    

    Or use Telescope/storage/logs for debugging instead of the browser page.

  • Vary: Accept becomes meaningless. You are effectively ignoring content negotiation, so any HTTP cache or CDN in front of the app can no longer distinguish representations. Usually a non-issue for token-authed APIs; notable if you serve public, cacheable endpoints.

  • Third-party packages branch on expectsJson(). Debugbar, some media/file libraries, and pagination helpers behave differently when it flips to true. If something starts returning JSON where it used to return a redirect or a download, this middleware is a prime suspect — test with $this->withoutMiddleware() to isolate.

  • Downloads and streams are unaffected, but plain response($html) will happily return HTML with a 200. If you want to be defensive, add a response-side check that sets Content-Type: application/json on anything that returns an array/Arrayable.

  • Vendoring is still a legitimate option — it is no longer a necessity. As of 3.2.0 the package tracks current Laravel (Laravel 13 compatibility landed, bot-driven housekeeping continues), so depending on it is fine for a normal upgrade cadence. But the entire behaviour remains one class:

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

    Copy it into app/Http/Middleware if you want to own it outright, add per-path exclusions, or force an X-Requested-With: XMLHttpRequest header alongside Accept. The trade-off is explicit: vendoring removes the composer constraint but also removes the free Laravel 13 bump you just got — with a five-line class that is a trivial diff to maintain yourself.

  • Debugging tip: when you can't tell whether the middleware ran, dump $request->headers->get('Accept') inside a route or check php artisan route:list -v for the group assignment. If you see HTML errors, the middleware is almost always registered after the middleware that threw.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core