Setting Up OpenAPI in Symfony with NelmioApiDocBundle the Right Way
AI generated
{ }
GET
REST API · OpenAPI · Symfony · NelmioApiDocBundle
OpenAPI in Symfony with NelmioApiDocBundle
set up cleanly, from zero to Swagger UI

Anyone running a Symfony REST API without documentation leaves the tedious reverse engineering work to consumers and QA teams. NelmioApiDocBundle generates a complete OpenAPI 3.1 specification from PHP attributes and YAML configuration, including Swagger UI, JWT security schemes, and response schemas straight from the code.

15 min read NelmioApiDocBundle · PHP Attributes · OpenAPI 3.1 · JWT · Swagger UI Symfony 6.x · 7.x · PHP 8.2+

1. Why NelmioApiDocBundle instead of manual YAML specs

REST API documentation maintained by hand as a YAML file goes stale with the first commit that skips a doc update. In practice, this creates discrepancies between the spec and actual behavior: an endpoint suddenly expects an extra required field, the Swagger file knows nothing about it, and the first consumer finds out via a 400 error. NelmioApiDocBundle solves this problem by deriving the OpenAPI specification directly from the PHP code, from routing metadata, PHP attributes on controllers, and typed request/response classes.

The second advantage lies in IDE support. PHP attributes are type-safe, and PhpStorm shows autocompletion and validation errors right while you type. Anyone who has edited YAML in the past and introduced typos that only surfaced at deployment time appreciates the static checkability immediately. The bundle also integrates seamlessly with Symfony Security, recognizes firewall-protected routes, and can automatically carry their security requirements over into the spec.

Third, the Swagger UI that ships with the bundle is directly reachable from the Symfony development server and always shows the current state of the spec, without a separate build step, without file synchronization. For teams working on backend and frontend at the same time, that is a clear efficiency gain over static spec files sitting in the repository.

2. Installation and bundle configuration

Installation happens via Composer. The bundle requires at least PHP 8.1 and Symfony 6.0. From PHP 8.2 onward, using PHP attributes instead of annotations is recommended, since annotations (Doctrine-style with @OA\...) are no longer preferred in Symfony 7. After installation, Symfony Flex automatically registers the bundle in config/bundles.php and creates a base configuration under config/packages/nelmio_api_doc.yaml.

The bundle's configuration splits into two areas: the global API metadata (title, version, description, server URLs) and the routing filters that determine which endpoints get included in the spec. The latter matters for excluding internal admin routes or health-check endpoints from the public documentation. The bundle supports multiple named sections (areas), so you can generate separate specs for a public API and an internal admin API from the same codebase.


# Install NelmioApiDocBundle via Composer
composer require nelmio/api-doc-bundle

# Install Swagger-UI assets (needed for the web UI)
composer require symfony/asset

# Optionally: install form type support for request body inference
composer require symfony/form

# Generate config skeleton if not auto-generated
php bin/console config:dump nelmio_api_doc

# config/packages/nelmio_api_doc.yaml
nelmio_api_doc:
  documentation:
    info:
      title: "Mironsoft REST API"
      description: "Public API for Mironsoft platform services"
      version: "1.0.0"
    servers:
      - url: "https://api.mironsoft.de/v1"
        description: "Production"
      - url: "http://localhost:8000/v1"
        description: "Development"
    components:
      securitySchemes:
        bearerAuth:
          type: http
          scheme: bearer
          bearerFormat: JWT
  areas:
    public:
      path_patterns:
        - "^/api/v1"
      host_patterns: []
    admin:
      path_patterns:
        - "^/api/admin"

3. OpenAPI attributes in Symfony controllers

Since PHP 8.0, native attributes are the preferred way to attach OpenAPI metadata directly to controller methods. The OpenApi\Attributes namespace of the zircote/swagger-php package, which NelmioApiDocBundle uses internally, provides all the attributes you need: #[OA\Get], #[OA\Post], #[OA\Parameter], #[OA\RequestBody], and #[OA\Response]. These attributes sit directly on the controller action and precisely describe inputs, outputs, and possible error responses.

Choosing the right level of abstraction matters. Individual parameters like path IDs or query filters belong as #[OA\Parameter] on the action. Request bodies with complex JSON structures, on the other hand, should reference their own schema classes decorated with #[OA\Schema]. Anyone who describes every field name inline in the controller attribute ends up with unreadable attribute blocks and loses the reusability of schemas across multiple endpoints.


<?php
// src/Controller/Api/V1/ProductController.php
declare(strict_types=1);

namespace App\Controller\Api\V1;

use OpenApi\Attributes as OA;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Attribute\Route;

#[OA\Tag(name: 'Products')]
#[Route('/api/v1/products')]
class ProductController extends AbstractController
{
    /**
     * Retrieve a single product by its ID.
     */
    #[OA\Get(
        path: '/api/v1/products/{id}',
        summary: 'Get product by ID',
        parameters: [
            new OA\Parameter(name: 'id', in: 'path', required: true,
                schema: new OA\Schema(type: 'integer', minimum: 1))
        ],
        responses: [
            new OA\Response(response: 200, description: 'Product found',
                content: new OA\JsonContent(ref: '#/components/schemas/ProductResponse')),
            new OA\Response(response: 404, description: 'Product not found',
                content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
        ]
    )]
    #[Route('/{id}', methods: ['GET'])]
    public function show(int $id): JsonResponse
    {
        // ... controller logic
    }
}

4. Request and response schemas with PHP classes

Reusable OpenAPI schemas are best defined as dedicated PHP classes or DTOs annotated with #[OA\Schema]. Each property of the class gets an #[OA\Property] attribute with type, description, and optional validation information such as minimum, maxLength, or enum. These classes sensibly live in the App\Api\Schema\ or App\Dto\ namespace, separate from entities and services.

A decisive advantage of this approach: the same DTO classes used for the OpenAPI spec can also serve as Symfony form types or as targets for the Symfony Serializer. Combined with the Symfony Validator, validation rules and API schema are defined in a single place, and both, runtime validation and generated documentation, stay automatically in sync. That is the core idea of the code-first approach versus the design-first alternative.

5. Security schemes: configuring JWT Bearer and API key

NelmioApiDocBundle supports all security scheme types defined in OpenAPI 3.x: HTTP Bearer (for JWT), API key (header or query parameter), and OAuth2 with various flows. Configuration happens either globally in the bundle's YAML configuration (for scheme definitions) or per endpoint as an #[OA\Security] attribute (to specify which scheme the given endpoint requires). Routes protected by Symfony's firewall with stateless: true and JWT must be explicitly annotated with #[OA\Security(name: 'bearerAuth')], since the bundle does not automatically evaluate the firewall configuration.

In Swagger UI, the Authorize button then appears in the top right, letting testers enter a JWT token. The token gets sent as an Authorization header for all subsequent try-it-out requests. For internal APIs it makes sense to define a second security section that uses a long-lived API key scheme, for example for machine-to-machine communication where a short-lived JWT token is not practical.


# config/packages/security.yaml (relevant JWT firewall snippet)
security:
  firewalls:
    api:
      pattern: ^/api/v1
      stateless: true
      jwt: ~

# In controller action, apply security requirement to endpoint:
# #[OA\Security(name: 'bearerAuth')]

# config/packages/nelmio_api_doc.yaml, global security default
nelmio_api_doc:
  documentation:
    security:
      - bearerAuth: []
    components:
      securitySchemes:
        bearerAuth:
          type: http
          scheme: bearer
          bearerFormat: JWT
        apiKey:
          type: apiKey
          in: header
          name: X-API-KEY

6. API groups and versioning

Larger APIs often consist of multiple versions or logical groups, a public v1 API for external consumers, a v2 API with breaking changes, and an admin API for internal management operations. NelmioApiDocBundle models this separation through named areas. Each area has its own path_patterns, its own security defaults, and is served under its own URL: /api/doc/public.json versus /api/doc/admin.json.

Versioning in the URL path (/api/v1/ vs. /api/v2/) is the most common strategy and easy to implement with the bundle. Anyone who prefers header-based versioning (Accept: application/vnd.mironsoft.v2+json) can implement it with custom request matchers, but must adjust the routing patterns accordingly. With URL versioning, it makes sense to organize routes in separate files and include them via config/routes/api_v1.yaml and config/routes/api_v2.yaml, so the bundle's area filtering works correctly.

7. Deploying Swagger UI safely in production

NelmioApiDocBundle's Swagger UI is reachable at /api/doc in development mode and usable immediately. In production, two things need attention: first, the route to Swagger UI should sit behind an authentication wall, nobody should be able to view the full API documentation, including security schemes, without logging in. Second, the dynamic spec generation causes CPU load on every request, since the bundle scans all controllers for attributes. In production, the spec should be cached or served as a static file.

The simplest safeguard is a separate Symfony firewall entry for /api/doc that uses HTTP Basic or an IP whitelist as authentication. For teams with a CI/CD pipeline, it makes sense to generate the OpenAPI spec as a build artifact (bin/console nelmio:apidoc:dump --area=public > public/api-spec.json) and have Swagger UI served statically from an Nginx instance that references the JSON file. This eliminates the runtime overhead entirely.

8. Exporting and testing the OpenAPI spec as JSON/YAML

NelmioApiDocBundle ships with a console command that exports the generated spec into various formats. bin/console nelmio:apidoc:dump --area=public --format=json outputs the complete OpenAPI 3.x specification as JSON on stdout. Piping into jq . lets you format the output, or use python3 -m json.tool to check syntactic correctness. The YAML export (--format=yaml) is easy to read and serves as a good basis for manual additions or as input for other tools.

For automated validation in the CI pipeline, swagger-cli validate api-spec.json or the spectral lint command from the Stoplight ecosystem is recommended. Spectral supports custom rule sets that let you enforce coding guidelines for the API spec: required fields in response schemas, forbidden field names, consistent naming conventions. This validation runs in seconds and prevents incomplete or inconsistent specs from landing on the main branch.

9. Configuration approaches compared

There are several ways to maintain OpenAPI documentation in a Symfony project. The choice of approach significantly affects maintainability, completeness, and integration effort.

Approach Advantage Disadvantage Recommendation
Manual YAML spec Full control, no PHP overhead Goes stale quickly, high sync effort Only for design-first with code generation
PHP Attributes (NelmioApiDocBundle) Always current, IDE support, reusability Attributes on the controller get verbose Recommended for existing Symfony apps
API Platform Fully automatic from entities Heavily opinionated, hard to customize Good for CRUD-heavy resource APIs
Spec generator from tests Spec directly from real behavior Complex, requires a dedicated test framework As a complement to NelmioApiDocBundle
Swagger-PHP alone No bundle, lightweight No Symfony integration, manual routes Only if no Symfony bundle is wanted

For most Symfony projects, combining PHP attributes with NelmioApiDocBundle plus CI-side Spectral validation is the most pragmatic path. The attributes stay close to the code, the spec is always current, and the validation step ensures quality standards are met, without a developer having to maintain a YAML file by hand.

Mironsoft

REST API design, Symfony development, and OpenAPI documentation

Need a Symfony REST API with complete OpenAPI documentation?

We set up NelmioApiDocBundle in existing Symfony projects, migrate outdated annotations to PHP attributes, and integrate spec validation into your CI/CD pipeline.

Bundle setup

Setting up installation, configuration, routing filters, and security schemes

Schema design

Request and response DTOs with complete OpenAPI schemas and validation

CI integration

Spectral linting, spec export as an artifact, and securing Swagger UI for production

10. Summary

NelmioApiDocBundle is the most pragmatic way to integrate OpenAPI 3.1 into Symfony projects without having to maintain a separate spec file. PHP attributes on controllers and schema classes keep documentation and code in sync. Named areas cleanly separate public and internal APIs. Security schemes for JWT Bearer and API key are defined in the bundle's YAML configuration and applied per endpoint via #[OA\Security] attributes. The console export command makes the spec available as a static artifact for CI validation and production delivery.

The most important step after setup is integrating spectral lint into the CI pipeline. Without automated validation, missing response schemas, undocumented error responses, and inconsistent field names creep in, and these are exactly the details that cost API consumers time during integration. A valid OpenAPI document is the foundation for mock servers, client code generation, and security audits, all of which build on the same spec.

NelmioApiDocBundle, the essentials at a glance

Installation

composer require nelmio/api-doc-bundle, Symfony Flex sets up configuration and routing automatically. PHP 8.1+ and Symfony 6.x required.

PHP attributes

#[OA\Get], #[OA\Post], #[OA\Schema] directly on the controller and DTO, no YAML sync needed, IDE validation included.

Security

Define Bearer JWT and API key in nelmio_api_doc.yaml, apply per route with #[OA\Security(name: 'bearerAuth')].

Production

Export the spec with bin/console nelmio:apidoc:dump, serve Swagger UI behind a firewall, and integrate Spectral validation into CI.

11. FAQ: OpenAPI in Symfony with NelmioApiDocBundle

1Does NelmioApiDocBundle support OpenAPI 3.1?
From zircote/swagger-php 4.7+ and bundle version 4.19+. Check the version combination in composer.json, not every bundle version supports the full 3.1 feature set.
2Annotations and attributes at the same time?
Possible, but not recommended. Migrate fully per controller, mixed use leads to conflicts in the generated spec that are hard to debug.
3Exclude certain routes from the spec?
Via path_patterns in the area configuration or with explicit #[Security(name: null)] on the controller. Both approaches can be combined.
4Validate the spec automatically in CI?
spectral lint or swagger-cli validate. Export command: bin/console nelmio:apidoc:dump --format=json > api-spec.json. Fails with exit code 1 on OpenAPI violations.
5Document pagination in response schemas?
Create a dedicated PaginatedResponse class with #[OA\Schema]: a data array, total, page, and perPage as properties. Reference it via ref in all list endpoints instead of defining it inline.
6Document multiple API versions at the same time?
Yes, via named areas with their own path_patterns. Each area has its own URL (/api/doc/v1.json, /api/doc/v2.json) and can have different security defaults.
7Avoid runtime overhead in production?
Export the spec at deployment time and serve it as a static JSON file. Disable the dynamic /api/doc route in production or restrict it by IP.
8Document file uploads?
#[OA\RequestBody] with content type multipart/form-data and a schema with type: object and a binary property. Swagger UI renders this as a file upload field.
9Minimum PHP version?
NelmioApiDocBundle 4.x requires PHP 8.1+. In new projects with PHP 8.2+, always use native attributes instead of Doctrine annotations.
10Combine NelmioApiDocBundle with API Platform?
API Platform has its own OpenAPI generation and does not depend on NelmioApiDocBundle. Using both together leads to conflicts, you need to decide on one approach.