liquidrazor/dto-api-bundle
A Laravel bundle for building API layers around data transfer objects. It helps structure request and response payloads, keep controllers thin, and maintain consistent, typed data flow between your application and its API endpoints.
Read this first: liquidrazor/dto-api-bundle is a Symfony bundle, not a Laravel package. There is no service provider, no facade, no config/dto-api.php. You cannot composer require it into a Laravel app and expect it to boot. Your day-to-day leverage as a Laravel developer is in two places: (a) you work in a polyglot org where a Symfony service exposes an API built with it, or (b) you mine it as a reference implementation for a DTO-first JSON API in Laravel.
If you have a Symfony app to try it in:
composer require liquidrazor/dto-api-bundle — pin the exact version, given zero stars and zero dependents.config/bundles.php and check config/packages/ for the bundle's config tree (run bin/console config:dump-reference <bundle_alias> to discover the alias rather than guessing it).vendor/liquidrazor/dto-api-bundle/src/ in this order: the DTO base class/interface, the PHP attributes used to describe fields, the response/streaming factory, and the OpenAPI generator. Those four files are the whole mental model.bin/console debug:router | grep -i doc) to confirm the schema round-trips.If you only have Laravel: clone the repo and read it as a spec. The bundle's value to you is the opinion that a DTO is the single source of truth for validation, serialization, and OpenAPI. You will re-implement that with spatie/laravel-data + dedoc/scramble, and the bundle shows you the seams to design around.
One DTO per endpoint, thin controllers. The bundle's contract is that the DTO shape is the API shape. In Laravel terms:
final class StoreOrderData extends Data
{
public function __construct(
public string $customer_email,
/** @var LineItemData[] */
public array $lines,
) {}
}
Keep the controller to StoreOrderData::from($request)->toArray() or a single service call. If you find yourself transforming in the controller, the DTO is wrong.
Streaming as the default for collections. The bundle's streaming responses map cleanly onto response()->streamJson() in Laravel 10+:
return response()->streamJson(
(function () use ($query) {
foreach ($query->lazy(500) as $row) {
yield OrderData::from($row);
}
})(),
headers: ['X-Accel-Buffering' => 'no'],
);
Use lazy()/cursor() behind the generator, never get(). The bundle's whole point is that the DTO is the unit of emission, so each yield should be a valid, fully-shaped record — not a partial row.
OpenAPI from types, not annotations. The bundle derives schemas from DTO property types and attributes. In Laravel, dedoc/scramble does the same inference, so the discipline transfers: annotate arrays with @var Foo[], avoid mixed, and let the tooling fail loudly on untyped properties. Wire a CI check that diffs the generated spec against a committed copy so drift is caught in review.
Consuming the generated spec. If the Symfony service is upstream, pull its /api/doc.json into your Laravel BFF as a build artifact and generate typed clients from it (openapi-generator, or hand-rolled casts to your laravel-data classes). Contract tests against that snapshot catch upstream breaking changes before deploy.
Port checklist. Attributes that mark a field as read-only/write-only/grouped map to laravel-data's #[MapInputName], #[MapOutputName], and #[Groups]. Request validation belongs in DTO rules, not FormRequest, if you want the bundle's one-source-of-truth property.
"repositories": [{"type": "vcs", ...}]) and read every line before trusting it in production. Assume no upstream fixes.DtoApiServiceProvider. Don't try to auto-register it, don't expect Laravel's container to resolve its services, and don't expect config() access to bundle config. If you need it in a Laravel app, you are writing an adapter, not integrating.X-Accel-Buffering: no and confirm your load balancer doesn't re-buffer. Test through the real edge, not artisan serve.memory_get_peak_usage() on 100k rows.mixed, and untyped array elements degrade to {} or anyOf blobs in the generated spec. Treat a spec diff as a review gate, not a post-deploy curiosity.bin/console debug:router, debug:container --tag=<bundle_tag>, and config:dump-reference will tell you more than the README. In Laravel, the equivalents are php artisan route:list and a hard look at the committed spec.How can I help you explore Laravel packages today?