stfalcon-studio/api-bundle
A Symfony bundle for building REST APIs with standardized JSON responses, exception handling, and request/response helpers. It streamlines controller code and integrates with Symfony's serializer and validator to cut boilerplate.
This is a Symfony bundle, not a Laravel package. There is no service provider, no config/api-bundle.php to publish, and composer require into a Laravel app will install dead code. The realistic use cases for a Laravel developer are: (a) you maintain a Symfony service alongside Laravel and want a thin API layer, or (b) you are reading it as a reference implementation for base controllers, a JSON response factory, and a central exception subscriber.
If you are in Symfony:
composer require stfalcon-studio/api-bundle
Register the bundle manually — there is almost certainly no Flex recipe, and with 10 stars you should not assume one exists:
// config/bundles.php
return [
// ...
StfalconStudio\ApiBundle\StfalconStudioApiBundle::class => ['all' => true],
];
Check the actual class name first, because bundle naming is inconsistent across packages:
ls vendor/stfalcon-studio/api-bundle/src/*Bundle.php
Where to look first. The package is small enough to audit completely in under an hour. Read the src/ tree in this order:
src/DependencyInjection/ — tells you the config alias and what is auto-registered. If there is an Extension + Configuration pair, that is your config surface; if not, there is nothing to configure and services are wired by convention.src/Controller/ — the base controller(s) and traits you are meant to extend/use.src/Service/ — the response factory, paginator, or similar helpers.src/Exception/ and src/EventSubscriber/ — the error contract, which is the part most likely to conflict with your existing stack.First real use case. Extend the bundle's base controller and return a consistent JSON envelope:
use StfalconStudio\ApiBundle\Controller\AbstractController;
final class PingController extends AbstractController
{
public function __invoke(): Response
{
return $this->responseFactory->createResponse(['pong' => true]);
}
}
Then throw the bundle's API exception from a service and confirm it is converted to the expected status/shape:
throw new ApiException('Not found', Response::HTTP_NOT_FOUND);
If that round-trips through the subscriber correctly, the bundle is doing its job. If you are on Laravel, the equivalents to evaluate are a base Controller with a respond() helper, a Handler.php mapping domain exceptions to status codes, and API Resources — you can reproduce the whole bundle's value in roughly 100 lines, which is the honest benchmark for whether to adopt it.
Controller traits over inheritance. The typical pattern here is a thin AbstractController plus composable traits (EntityManagerTrait, ValidatorTrait, SerializerTrait, request helpers). Use the traits, not deep inheritance — it keeps you free to extend Symfony's own AbstractController if you need render(), redirectToRoute(), etc.
final class OrderController extends AbstractController
{
use EntityManagerTrait;
use ValidatorTrait;
public function create(Request $request): Response
{
$dto = $this->serializer->deserialize($request->getContent(), CreateOrder::class, 'json');
$this->validate($dto); // throws on violation
// ...
return $this->responseFactory->createResponse($order, Response::HTTP_CREATED);
}
}
One response factory, everywhere. The whole point is that success and error payloads share a shape. Never hand-roll new JsonResponse(...) in a controller; route everything through the factory (or a decorated version of it) so headers, envelope keys, and status codes stay uniform. This is also what makes your functional tests stable.
Central exception handling. The subscriber is the integration seam. Decide once whether the bundle's subscriber or Symfony's ErrorListener owns the response, and configure priority accordingly. A typical setup: domain exceptions implement a common interface carrying an HTTP status; the subscriber maps them; unexpected \Throwable falls through to a 500 with a generic body and a logged correlation ID.
Pagination. If the bundle ships a paginator/Page object, use it for both Doctrine and array-backed collections so clients see one pagination schema. Pass the page object straight to the response factory; do not leak Doctrine Paginator internals into the payload.
Doctrine-side helpers (timestampable, UUIDs) are convenient but are the most opinionated part. If your project already has gedmo/doctrine-extensions or a UUID strategy, do not mix them — pick one and remove the other's listeners.
Wiring and integration. Autowire the services in config/services.yaml:
services:
_defaults:
autowire: true
autoconfigure: true
StfalconStudio\ApiBundle\:
resource: '../vendor/stfalcon-studio/api-bundle/src/'
exclude: '../vendor/stfalcon-studio/api-bundle/src/{Entity,DependencyInjection}'
Only do this if the bundle does not already register its own services; double-registering produces confusing "service is private" or duplicate-subscriber behavior.
Testing. Use WebTestCase and assert on JSON paths, not on raw strings. Write at least one test per status class (200, 201, 400/422, 404, 500) so a bundle upgrade that changes the envelope fails loudly.
It is Symfony-only, and the adoption signal is weak. 10 stars, 0 dependents, a single maintainer. Treat it as a small internal library you are vendoring: pin an exact version in composer.json, commit composer.lock, and write contract tests around the response envelope and exception mapping before you depend on them. A fork is a realistic exit plan.
Verify the release. The listed last release date is in the future relative to most dependency graphs; before pinning, run composer show stfalcon-studio/api-bundle --all and confirm tags actually exist and match the branch you expect. Also check the require block for the PHP and Symfony version constraints — a bundle this small often lags a major Symfony release.
Bundle class name mismatch. The repository is ApiBundle but the PHP class may be StfalconStudioApiBundle or ApiBundle. A wrong entry in config/bundles.php gives a confusing "class not found" at cache warmup. debug:container and bin/console about confirm registration.
Config alias guessing. If there is an Extension, the alias is derived from the bundle name (e.g. stfalcon_studio_api), not the package name. Loading config/packages/api.yaml with the wrong root key silently does nothing — Symfony does not error on unknown root keys under a bundle alias unless you enable strict config. Grep getAlias() in the extension.
Trait method collisions. Multiple helper traits plus your own base class is a classic fatal error (Trait method X has not been applied, because there are collisions). Resolve with insteadof/as, or alias your own method. This bites hardest when two bundles expose same-named traits.
Subscriber ordering. Registering the bundle's exception subscriber alongside API Platform, FOSRestBundle, or a custom kernel.exception listener produces double-handled responses or a generic 500 swallowing your 422. Inspect with:
bin/console debug:event-dispatcher kernel.exception
Then set explicit priorities — usually your domain mapper first, the bundle's fallback last.
Envelope lock-in. The factory's success/error shape becomes a public API the moment clients ship against it. If it wraps everything in {"data": ...} and your existing clients expect a bare object, override the factory (decorate it, don't edit vendor) rather than special-casing in controllers.
Doctrine coupling. Entity-manager traits and timestampable/UUID listeners assume Doctrine ORM. None of it ports to Eloquent, and in a Laravel context only the conceptual patterns transfer. Do not attempt to bridge the two.
Validation shape. Decide whether validation failures are 400 or 422 and whether the payload lists all violations or the first one. The bundle and Symfony's own #[MapRequestPayload] may disagree; pick one path and disable the other.
Debugging checklist. bin/console debug:container StfalconStudio to see what was actually registered; debug:router after adding a controller to confirm the base class did not break route loading; lint:container in CI to catch wiring errors before deploy.
Extension points worth using. Decorate the response factory rather than subclassing it; implement the exception interface for your domain errors rather than editing the subscriber; override service definitions in services.yaml via decorates: when you need to replace behavior. Anything that requires editing vendor/ is a fork decision, not a config decision.
How can I help you explore Laravel packages today?