kununu/rest-api-bundle
kununu/rest-api-bundle is a Symfony bundle for PHP that packages reusable infrastructure and conventions for building REST APIs, helping teams standardize endpoints, request handling, and responses across services.
Read this before composer require: it is not a Laravel package, and it is not really a public package.
kununu/rest-api-bundle is a Symfony bundle. It has no service provider, no Laravel auto-discovery entry, and no integration with Laravel's container, router, request lifecycle, or validation. It sits on top of mediamonks/rest-api-bundle, which itself hooks into Symfony's HttpKernel (event subscribers, ParamConverter, DI compiler passes, annotation/attribute reading). In a Laravel app, composer require will pull a pile of symfony/* components and change nothing about how your app behaves. The zero stars / zero dependents confirms what the description implies: this is kununu's internal bundle published to Packagist so their own services can install it, not a package with an outside user base.
So the honest "getting started" is two paths:
If you are evaluating this for a Laravel service: stop. Use the Laravel-native equivalents table in the next section. Nothing here is worth the impedance mismatch.
If you are inheriting or auditing a Symfony service that already depends on it:
composer.json first — the symfony/* constraints, the pinned mediamonks/rest-api-bundle version, and the PHP floor. That tells you whether it is even installable on a current stack.composer require kununu/rest-api-bundle.config/bundles.php (or AppKernel), parent first.vendor/mediamonks/rest-api-bundle/README.md. Read that end to end. Then diff vendor/kununu/rest-api-bundle/src against it — the kununu delta is thin and largely undocumented, so the diff is the documentation.POST /api/things deserializing into a DTO, validating it, and returning 201 with a serialized representation. If that round-trips, the wiring is correct.The bundle's value is a request-DTO → domain → view-model → serialized-response pipeline, with serializer groups, pagination, problem+json errors, and HTTP caching handled by convention rather than per-controller code.
1. The canonical controller. Deserialize the body into a DTO (via the bundle's request-body param converter / annotation), hand it to your domain service, return a view model through the response builder:
/**
* @RequestBody(class="App\Dto\CreateThing")
*/
public function create(RequestBody $body, ResponseBuilder $builder, Request $request): Response
{
$thing = $this->service->create($body);
return $builder->build(new ThingView($thing), $request);
}
The view model implements the bundle's ViewModelInterface; the builder turns it into a Response and hands it to symfony/serializer with the right groups.
2. Serializer groups as API versions. Annotate view-model properties with groups (api:v1, api:detail), and let the request select a group set — typically a ?view= query parameter. This is the bundle's answer to versioning and to "don't leak internals."
3. Pagination as a contract, not a habit. Accept the bundle's pagination object off the request, return a paginated response wrapping the collection plus metadata. Consumers get stable page/limit/total/_links shapes without each endpoint inventing its own.
4. Transformers for entities you don't control. If you can't annotate a Doctrine entity, register a TransformerInterface for it (tagged in DI) and the transformer pool will apply it during serialization. Prefer this over hand-rolled toArray() methods.
5. Errors are a listener, not try/catch. Throw exceptions implementing the bundle's exception interface; a kernel.exception subscriber converts them (and symfony/validator violations) into a consistent error payload. Add new error types by implementing the interface, not by editing controllers.
6. Caching headers. View models implementing the cacheable interface get ETag/Last-Modified computed and 304 handling applied automatically.
Mapping to what you'd actually build in Laravel:
| Bundle concept | Laravel equivalent |
|---|---|
| Request-body DTO + deserialization | FormRequest / spatie/laravel-data |
| View model + serializer groups | JsonResource with conditional attributes |
?view= group selection |
?include= via spatie/laravel-query-builder |
| Paginated response contract | ResourceCollection over paginate() |
| problem+json error listener | bootstrap/app.php exception rendering |
| ETag / Last-Modified | $response->setEtag() + middleware |
| Transformer pool | Container-bound resources / Resource::collection |
mediamonks/rest-api-bundle alone. Only then blame the kununu layer.composer outdated will likely show it pinned to an old Symfony major. If you depend on it long-term, vendor/fork it and own the code.?view= is user-controlled. If group selection comes from the query string, an attacker can request a group you didn't intend to expose. Allowlist the accepted views in a request listener.api:v1 in two unrelated view models collide silently. Namespace your groups per bounded context.@MaxDepth, or explicit group boundaries — and watch for the N+1 queries the serializer triggers.POST often yields a null DTO rather than a validation error; a wrong Content-Type can be silently ignored. Add an explicit null check or a kernel.request guard.validation_groups. A DTO validated on Default will skip constraints you put in a named group — a class of bug that looks like "validation doesn't work."Vary: Authorization and be conservative with the cacheable interface.getPriority(). A stray kernel.exception listener in your own app can swallow the JSON format and hand the client an HTML error page.bin/console debug:event-dispatcher kernel.exception, debug:router, and debug:container --tag=<transformer tag> to confirm your transformers and listeners are actually registered.kernel.response listener for cross-cutting headers (request ID, rate limits). Prefer these over subclassing.JsonResource and FormRequest. Copying the Symfony machinery gets you a second framework inside your first one.How can I help you explore Laravel packages today?