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

Api Response Bundle Laravel Package

mattjanssen/api-response-bundle

A PHP bundle for building consistent API responses, with helpers for standardized success and error payloads so your application returns clean, predictable JSON across endpoints.

View on GitHub
Deep Wiki
Context7

Getting Started

First, the honest framing: this is a Symfony bundle, not a Laravel package. There is no service provider, no config/api_response.php, and no composer require path into a Laravel app — its composer.json pulls in symfony/symfony and sensio/framework-extra-bundle and registers itself via bundles.php. Treat it as a reference implementation for a pattern, not a dependency.

What v1.0.0 changes (2025-… release): the constraint widening is the whole story. Nothing in the API surface moved; the package just stopped being pinned to Symfony 2.3/Sensio 3.

Constraint Before After v1.0.0
php ^5.4|^7 ^5.4|^7 (unchanged)
symfony/symfony ^2.3|^3 ^2.4|^3|^4|^5
sensio/framework-extra-bundle ^3|^4 ^3|^4|^5
mockery/mockery (dev) ^0.9.5 ^0.9.5|^1
jms/serializer-bundle (dev) ^1 ^1|^2|^3
phpunit/phpunit (dev) ^4.0 replaced by symfony/phpunit-bridge
phpoption/phpoption (dev) — added ^1.1

Three things to take from that table before you read any code:

  1. PHP 8 is still unsupported. ^5.4|^7 is unchanged, so this will not install on PHP 8.0+ without patching composer.json. Every Laravel since 9.x requires PHP 8.0. That is the hard stop, and v1.0.0 did not remove it.
  2. Symfony 5.4 is the ceiling, and 5.4 is EOL. Security support for Symfony 5.4 ended in November 2024. So the "widened" range is entirely composed of end-of-life branches. v1.0.0 buys you compatibility with the past, not the present.
  3. sensio/framework-extra-bundle is the anchor. It was deprecated in Symfony 5 and removed in Symfony 6. As long as this package requires it in require (not require-dev), Symfony 6/7 support is structurally impossible without a rewrite.

Where to look first (the repo is small enough to read end-to-end in ~20 minutes):

  1. composer.json — the table above is the first hard stop. Also note the release note text is mildly contradictory ("bumps the minimum to v2.4" alongside a diff that adds ^4|^5); trust the diff, not the prose.
  2. DependencyInjection/Configuration.php — the bundle's real API surface. Every config key you'd care about is in that tree; ignore the README if it drifts.
  3. Resources/config/services.* — which listeners/subscribers are registered and at what priority.
  4. The listener classes themselves — expect roughly two: one on kernel.view (transforms controller return values) and one on kernel.exception (transforms thrown exceptions). Those two hooks are the entire bundle.

The first use case it encodes, and how you'd satisfy it in Laravel today:

A controller returns a plain array or DTO; the framework turns it into a consistent JSON envelope. A thrown exception becomes a JSON error body with a sensible status code.

Laravel gives you the first half for free:

// Already JSON in Laravel — no bundle needed
public function show(Order $order)
{
    return ['id' => $order->id, 'total' => $order->total]; // 200 + application/json
}

The second half is ~20 lines in the exception handler; the envelope is a one-line macro. Baseline you should have before reading further:

// app/Providers/ResponseServiceProvider.php
Response::macro('api', function ($data, int $status = 200, array $meta = []) {
    $payload = ['data' => $data];
    if ($meta !== []) {
        $payload['meta'] = $meta;
    }
    return response()->json($payload, $status);
});

The v1.0.0 conclusion is the same as before with more confidence: write down what the bundle gives you that these two snippets don't. For most Laravel apps the answer is "nothing structural — only consistency."

Implementation Patterns

The bundle's three moving parts, and the Laravel pattern each one maps to.

1. Return-value transformation (kernel.view)

Symfony fires kernel.view only when a controller returns something that is not a Response. The bundle listens there, serializes the value, wraps it in its envelope, and returns a JsonResponse. The Laravel equivalent is simply not returning a Response and letting the router JSON-encode arrays — plus JsonResource when you need shape control:

// Shape control the bundle would give you via its transformer
class OrderResource extends JsonResource
{
    public function toArray($request): array
    {
        return [
            'id'    => $this->id,
            'total' => $this->total,
            'links' => ['self' => route('orders.show', $this)],
        ];
    }
}

// Controller
return new OrderResource($order);          // 200
return (new OrderResource($order))->response()->setStatusCode(201);

If you want a single envelope for every response, apply it at the factory level rather than per-controller — JsonResource::wrap('data') in a service provider:

// app/Providers/AppServiceProvider.php  boot()
JsonResource::wrap('data');

Note the v1.0.0 dependency change is relevant here: jms/serializer-bundle ^1|^2|^3 is in require-dev, which tells you the bundle's transformation path is exercised against JMS Serializer as its normalizer. That is the "swap the serializer" extension point — in Laravel you get the same by binding a single Transformer contract (see §3), not by pulling in JMS.

2. Exception transformation (kernel.exception)

This is where the bundle earns its keep: a map from exception type → status code → error body, so domain exceptions don't need abort() sprinkled through the codebase. Port it as a status map in the handler, not as per-exception render closures:

// bootstrap/app.php  (Laravel 11+)
->withExceptions(function (Exceptions $exceptions) {
    $exceptions->render(function (Throwable $e, Request $request) {
        if (! $request->expectsJson()) {
            return null; // let Laravel render HTML/redirect as normal
        }

        $status = match (true) {
            $e instanceof ValidationException      => 422,
            $e instanceof AuthenticationException  => 401,
            $e instanceof AuthorizationException   => 403,
            $e instanceof ModelNotFoundException   => 404,
            $e instanceof DomainException          => 409,
            default                                => 500,
        };

        return response()->json([
            'error' => [
                'type'    => class_basename($e),
                'message' => $status === 500 && ! config('app.debug')
                    ? 'Internal Server Error'
                    : $e->getMessage(),
            ],
        ], $status);
    });
});

Two things the Symfony listener does that are easy to get wrong when porting:

  • It runs before the framework's own error listener. In Symfony that's an explicit listener priority on KernelEvents::EXCEPTION. In Laravel the equivalent is that your render closure is consulted before the default rendering — which it is — but expectsJson() must be checked or you'll break HTML flows and redirects.
  • It preserves HttpException status codes. Don't blanket-map to 500; unwrap HttpExceptionInterface first.

3. Serialization / normalizer swap

The bundle will have a transformer or normalizer service you can override (that's the extension point — see Gotchas). In Laravel the analogue is swapping JsonResource for something else at the container level, or writing one ApiTransformer contract and binding it:

// app/Contracts/Transformer.php
interface Transformer { public function transform(mixed $value): array; }

// app/Providers/AppServiceProvider.php
$this->app->bind(Transformer::class, DefaultTransformer::class);

Then a single middleware converts arrays to the envelope, so controllers stay thin:

class EnvelopeJsonResponses
{
    public function handle($request, Closure $next)
    {
        $response = $next($request);

        if ($response instanceof JsonResponse && $request->is('api/*')) {
            $data = $response->getData(true);
            if (! isset($data['data'], $data['error'])) {
                $response->setData(['data' => $data]);
            }
        }

        return $response;
    }
}

Port checklist if you decide to lift the design wholesale: envelope shape → macro or middleware; exception map → withExceptions; content negotiation → expectsJson(); status codes for POST/DELETE → explicit in the controller (the bundle likely defaulted to 200 for everything).

Gotchas and Tips

Still don't depend on it — but the reason has shifted. Pre-v1.0.0 the objection was abandonment (7 stars, 0 dependents, last release 2019-12-04). Post-v1.0.0 the objection is stale reach: PHP ^5.4|^7 unchanged (no PHP 8), Symfony ceiling at EOL branches (2.4 through 5.4), and a hard requirement on sensio/framework-extra-bundle, which Symfony 6 removed. v1.0.0 is a compatibility-lane release, not a revival. Read it, port the ideas, delete the dependency.

symfony/symfony is itself an anti-pattern. The constraint is on the metapackage, not individual components. That means you cannot mix and match — install this alongside any other Symfony package with a conflicting component constraint and composer will fail on the metapackage, not on the thing that actually conflicts. If you fork it to widen constraints, switch to symfony/framework-bundle while you're in there.

kernel.view is skipped the moment you return a Response. This is the classic "why isn't my envelope applied?" bug in Symfony — return new JsonResponse(...) from a controller and the whole bundle silently steps aside. The same trap exists in Laravel middleware: if a controller already returns a JsonResponse, your middleware sees it and may double-wrap it. Decide once whether the envelope is applied at the factory or the middleware, and never both.

Double-wrapping. ['data' => ['data' => ...]] happens when a resource already wraps and middleware wraps again. Guard on isset($data['data']) as above, or — better — skip middleware entirely and rely on JsonResource::wrap().

204 No Content must have an empty body. Serializers happily emit null or {} for a DELETE returning nothing, producing a body on a 204, which some clients and proxies reject. Short-circuit any transformer on 204/304.

Status codes are a contract, not a default. The bundle's failure mode is returning 200 for creates and deletes. If you port it, make 201 (with Location) and 204 explicit and covered by tests.

Content-Type drift. application/json vs application/vnd.api+json vs application/problem+json (RFC 7807). Pick one before you have consumers. If you're building something public, problem+json for errors is the one upgrade worth doing over the bundle's likely {"error": ...} shape.

Dev-mode exception pages win. In Symfony's debug environment the profiler/error page renders unless your listener outranks the framework's error listener. In Laravel the analogous surprise is that APP_DEBUG=true leaks stack traces through your custom renderer — always branch on app.debug for the 500 case.

Validate your error map against real exceptions. ModelNotFoundException is normally converted to a NotFoundHttpException by the router before it reaches Laravel's handler in some paths, so a match on the model exception may never fire. Log the $e::class in the renderer for a week and build the map from observed types.

Extension points to look for when reading it: a configurable status-code map in Configuration.php, an overridable transformer/normalizer service alias (override via services.yaml or a compiler pass in Symfony), and a header/content-type option. Anything not in that config tree was hardcoded — and hardcoding is what you should not copy.

Reading the v1.0.0 test setup as a portability signal. The move from phpunit/phpunit ^4.0 to symfony/phpunit-bridge and the addition of phpoption/phpoption suggest the package gained a real CI matrix across Symfony 3.4/4/5. That's the part worth borrowing: if you build your own envelope layer, the test matrix — one PHP version × one framework version per lane — is what keeps it honest. Note there is still no PHP 8 lane in that matrix, which is the single clearest signal of where the maintenance boundary sits.

Test the envelope, not the payload. One feature test per route asserting assertJsonStructure(['data' => [...]]) and one per exception class asserting status + error.type will catch every regression a bundle like this is meant to prevent. That test suite is the actual artifact worth taking from this package — more so now than the code, which v1.0.0 confirms is frozen in the PHP 7 era.

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.
alphabalex/messaging-made-easy
rawphp/warp
shootkiran/merobug
narakode/fineauth
numencode/wn-syncops-plugin
ibnnajjaar/mib-global-pay
ably/pubsub-server
mstfkhazaal/defender
mfonte/base62x
karnoweb/metapilot
tobya/bcsapiwrapper
swalker2/cpanel
vish4395/laravel-translation-manager
morningtrain/economic
teksite/systeminfo-laravel
atomcms/voting
run6/weather
siokas/laravelembeddirectives
adamlien/pkuuids
gilsonsouza/lara-doc-generator