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 Doc Bundle Laravel Package

nelmio/api-doc-bundle

NelmioApiDocBundle generates clean, accurate API documentation for your Symfony applications using PHP 8 attributes and OpenAPI 3.0, with support for FrankenPHP worker mode and long-running workers.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the bundle and let Flex wire it up:

composer require nelmio/api-doc-bundle

Flex registers Nelmio\ApiDocBundle\NelmioApiDocBundle in config/bundles.php and creates config/packages/nelmio_api_doc.yaml. Start with the minimum — an info block and a route area:

# config/packages/nelmio_api_doc.yaml
nelmio_api_doc:
    documentation:
        info:
            title: 'Acme API'
            version: '1.0.0'
    areas:
        default:
            path_patterns:
                - ^/api(?!/doc$)

Expose the UI (dev only) by importing one of the bundled routing files:

# config/routes/nelmio_api_doc.yaml
when@dev:
    nelmio_api_doc.swagger_ui:
        resource: '@NelmioApiDocBundle/config/routing/swaggerui.yaml'
        prefix: /api/doc

Then hit /api/doc and confirm your routes are discovered — the bundle picks up Symfony routes automatically, so you should see operations even before writing a single attribute. Where to look first: bin/console debug:router | grep nelmio for the UI route, and bin/console nelmio:apidoc:dump --format=json for the raw OpenAPI document. The JSON is the source of truth; the UI is just a viewer on top of it.

First real use case: annotate one controller action so a response is documented.

use OpenApi\Attributes as OA;
use Nelmio\ApiDocBundle\Attribute\Model;

#[OA\Get(
    path: '/api/users/{id}',
    summary: 'Fetch a single user',
    tags: ['Users'],
    parameters: [
        new OA\Parameter(name: 'id', in: 'path', required: true, schema: new OA\Schema(type: 'integer')),
    ],
    responses: [
        new OA\Response(
            response: 200,
            description: 'User found',
            content: new OA\JsonContent(ref: new Model(type: App\Dto\UserOutput::class, groups: ['user:read']))
        ),
        new OA\Response(response: 404, description: 'Not found'),
    ]
)]
public function show(int $id): JsonResponse { /* ... */ }

That's the 80% loop: route + #[Model] + serializer groups, verified by dumping JSON.

Implementation Patterns

DTO-first documentation. Do not hand-write schemas for payloads. Point #[Model] at the DTO you actually serialize and let the describer derive properties from typed properties, #[Groups], and validation constraints. One #[Model] per direction (input/output DTO), since write and read representations almost never match:

content: new OA\JsonContent(ref: new Model(type: UserInput::class, groups: ['user:write']))

// Collections: wrap, don't repeat
content: new OA\JsonContent(
    type: 'array',
    items: new OA\Items(ref: new Model(type: UserOutput::class, groups: ['user:read']))
)

Tag at class level, operation at method level. Put #[OA\Tag(name: 'Users')] and shared #[OA\Parameter] definitions on the controller (they inherit through abstract base controllers as of 5.2.1), and keep per-action #[OA\Get]/#[OA\Post] blocks near the code they describe. This keeps handlers readable while the OpenAPI tree stays coherent.

Split documentation by audience with areas. Areas are the mechanism for public API vs. internal admin vs. partner API. Filter by path or by attribute:

nelmio_api_doc:
    areas:
        default:
            path_patterns: [ ^/api/v1 ]
        internal:
            path_patterns: [ ^/api/internal ]
            with_attribute: true   # only routes carrying #[Areas] for this area

Then dump and serve each separately: nelmio:apidoc:dump --area=internal --format=json, and point a second UI route at ?area=internal.

Security from your authorization layer. Declare the schemes once in config, and let #[IsGranted] produce the per-operation security requirements:

nelmio_api_doc:
    areas:
        default:
            security:
                MyBearerScheme:
                    type: http
                    scheme: bearer

Every controller action guarded by #[IsGranted('ROLE_ADMIN')] then advertises the scheme automatically. For anything not covered by IsGranted, add #[OA\Security] (or #[Security]) explicitly — otherwise the operation silently appears public.

Contract artifacts in CI. The generated document is a build artifact, not a dev-only page. Dump it in the pipeline and diff it:

php bin/console nelmio:apidoc:dump --format=json --pretty > build/openapi.json

Use that file to generate typed API clients (openapi-generator, openapi-typescript), run schema linting, and fail the build on an unexpected diff. This turns documentation drift into a merge-blocking signal instead of a code review comment.

Swap the UI without touching the spec. Swagger UI, ReDoc, Stoplight Elements, and Scalar all consume the same JSON. Changing swaggerui.yaml for redocly.yaml/stoplight.yaml/scalar.yaml in your routing import is a one-line decision; keep it that way so marketing/docs-site preferences never leak into controller code.

Custom describers for value objects. When a domain type (money, UUID wrapper, enum with metadata) renders badly, register a ModelDescriberInterface / PropertyDescriberInterface implementation and tag it. Note the supports() signature gained an optional $context parameter in 4.33.5 — implement it if you support versioned area context. Prefer this over sprinkling #[OA\Schema] on hundreds of DTOs.

Use type_info: true on modern stacks. With symfony/type-info available, enable it so generic types (Collection<int, UserOutput>, iterable<Foo>) describe correctly instead of degrading to array:

nelmio_api_doc:
    type_info: true

Gotchas and Tips

Namespace collision is the #1 beginner error. OpenAPI attributes come from OpenApi\Attributes as OA; Nelmio's own attributes (Model, Areas, Security, Ignore) live under Nelmio\ApiDocBundle\Attribute. Mixing them up produces "attribute not found" or silently ignored attributes. Also note the 5.0 rename: Nelmio\ApiDocBundle\Annotation\* no longer exists — it's Attribute\*, and PHP annotations are gone entirely (PHP 8.1+, Symfony 6.4+).

Exactly one #[OA\Info] (or one config info block). Defining info in both config and an attribute, or on two different classes, throws during generation. Pick config for static metadata; only use the attribute if the value is derived from code.

Path parameters must be declared twice. The {id} placeholder in the route is not enough — you need an OA\Parameter with in: 'path', required: true and the schema type. Omit required: true and Swagger UI shows an optional path segment that no router will ever accept.

ref: only accepts objects, not strings. new OA\JsonContent(ref: User::class) will produce a broken document; it must be ref: new Model(type: User::class, groups: [...]) (or new OA\Schema(ref: '#/components/schemas/User') for hand-managed components).

Serializer groups and documented groups must be identical. #[Model(type: User::class, groups: ['user:read'])] describes what the serializer would emit under that group set. If your controller serializes with ['user:list'] but documents ['user:read'], the spec is confidently wrong and clients break at runtime. Keep the groups in a constant or enum and use it in both places.

Mismatched Model options are deprecated. Passing null as $options to Model::__construct() is deprecated — pass []. Likewise Areas(['value' => [...]]) is deprecated in favour of Areas(['foo', 'bar']), and with_annotation is superseded by with_attribute. These fire deprecation notices that will become breaking in the next major, so clean them up while you're in the file.

Circular references and #[Ignore]. Bidirectional entities (User -> Orders -> User) explode into recursion or produce gigantic nested schemas. Either document DTOs instead of entities, or exclude the back-reference with #[Ignore] (4.38+) on the property. #[Ignore] is property-level, not class-level — it won't hide the class itself.

Deduplication is by FQCN, and schema naming matters. Two classes with the same short name in different namespaces used to merge; that's fixed (FQCN is now the dedup key), but the displayed schema name is still derived from the short name, so you'll see User, User2. Pin explicit names when generating clients, otherwise a namespace refactor silently renames schemas and regenerates your client SDK.

Validation constraints get converted, not copied. #[Assert\Regex] patterns are translated from PCRE to ECMA-262 (improved in 4.38.2) so the OpenAPI pattern is valid for JS tooling. Translation is not perfect — lookarounds and possessive quantifiers may not survive. If a regex is load-bearing for client-side validation, verify it in the dumped JSON rather than assuming.

Areas regexes are unanchored by default. path_patterns: [ /api ] matches /admin/api-keys too. Always anchor: ^/api(?!/doc$). The negative lookahead is worth remembering — without it, the UI route documents itself.

Nullable + JMS used to wrap everything in oneOf. Fixed in 5.10, but if you're pinned below that, a nullable property suddenly becomes oneOf: [type, null], which breaks naive client generators. Upgrade rather than working around it.

Long-running workers (FrankenPHP, RoadRunner) need 5.11.1+. Earlier versions leaked state between requests during document generation. If you're on worker mode, also avoid mutating the documentation at runtime — build it once (nelmio:apidoc:dump) or accept a warm-up cost. Never rely on in-request caching of the doc object.

Debug from the JSON, not the browser. When an operation looks wrong, run nelmio:apidoc:dump --format=json | jq '.paths["/api/users/{id}"]'. If it's correct in JSON and wrong in the UI, you have a browser cache or an assets problem (assets:install, or the UI bundle's static files not published). If it's wrong in JSON, it's an attribute/describer problem.

Expect phantom duplicate operations. Routes with a {_format} suffix, routes duplicated across two controllers, or an #[OA\Get] whose path: disagrees with the actual route all yield extra operations. The attribute path is documentation only — it does not have to match, which is exactly why it rots. Keep it in sync or rely on route-derived paths where possible.

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