Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Symfony API Platform for REST and GraphQL

By Kokil Thapa | Last reviewed: August 2026

Building a dual-protocol backend often feels like maintaining two separate codebases, but Symfony API Platform for REST and GraphQL solves this by generating both endpoints from a single set of PHP resources. Instead of writing custom serializers or resolvers manually, you define your domain model once and let the framework handle content negotiation, validation, and documentation. This approach drastically reduces boilerplate while enforcing strict architectural standards across your application.

If you are evaluating backend frameworks for a new project, understanding this unified architecture is critical before committing to a stack. For teams already familiar with Laravel ecosystems, the transition involves shifting from explicit route definitions to declarative resource configuration. I have covered similar architectural decisions in my guide on Laravel API best practices, where we compare imperative versus declarative API design patterns. The core principle remains: let the framework handle protocol translation so you can focus on business logic.

How do you install and configure Symfony API Platform for REST and GraphQL?

Setting up the environment correctly prevents subtle runtime errors later. As of 2026, API Platform 4.x runs on Symfony 7.x and requires a minimum of PHP 8.2, though PHP 8.4 is the recommended stable release for production workloads due to significant JIT improvements and property hook support.

Initial Installation and Dependency Management

Start with the official distribution if you are creating a greenfield project. It includes Docker configurations, Mercure for real-time capabilities, and pre-configured CORS settings that save hours of debugging:

composer create-project api-platform/skeleton my-api-project
cd my-api-project
docker compose up -d

For existing Symfony applications, require the core bundle and the GraphQL extension separately. Note that GraphQL is no longer included by default in the skeleton to reduce footprint for pure REST projects:

composer require api-platform/core:^4.0
composer require api-platform/graphql:^4.0
php bin/console cache:clear

A common mistake I encounter during audits is missing the GraphQL package and assuming it ships with the core. Always verify your composer.json explicitly lists api-platform/graphql if you intend to expose that endpoint.

Verifying the Default Configuration

After installation, navigate to /api in your browser. You should see the Swagger UI / OpenAPI documentation auto-generated from your entity metadata. If this page returns a 404 or blank screen, check your config/routes/api_platform.yaml to ensure the resource directory mapping points to your actual entity namespace. In production environments, always disable the Swagger UI via environment variables and serve only the JSON specification to reduce attack surface.

Composer Requireapi-platform/core ^4.0Bundle RegistrationAuto-enabled in KernelRoute Generation/api & /graphqlReadyPHP 8.2+ Required
Installation pipeline for Symfony API Platform for REST and GraphQL showing dependency resolution and route registration steps

How do you expose Doctrine entities as REST and GraphQL resources?

The power of Symfony API Platform for REST and GraphQL lies in its attribute-driven configuration. You no longer need YAML mapping files or extensive controller boilerplate. A single PHP attribute transforms a standard Doctrine entity into a fully functional API resource.

Defining the ApiResource Attribute

In PHP 8.4, use native attributes to declare operations. This example exposes a LegalCase entity with specific REST operations and automatic GraphQL type generation:

<?php
namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Post;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
#[ApiResource(
    operations: [
        new Get(),
        new GetCollection(),
        new Post(validationContext: ['groups' => ['create']]),
    ],
    graphql: [
        'item_query' => true,
        'collection_query' => true,
        'mutation' => ['create' => true],
    ]
)]
class LegalCase
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 255)]
    public string $title;

    #[ORM\Column(type: 'text')]
    public string $description;
}

This configuration generates GET /legal_cases/{id}, GET /legal_cases, and POST /legal_cases REST endpoints alongside equivalent GraphQL queries and mutations. The framework handles serialization groups, input validation, and response formatting automatically based on these declarations.

Customizing Serialization and Validation

Never expose internal database fields directly. Use Symfony Serializer groups to control output shape. Create separate groups for read and write operations to prevent mass assignment vulnerabilities. On legal-tech portals I have built, we always separate case:read from case:write to ensure sensitive client notes never leak through collection endpoints even when accessible individually.

How does security and authorization work in API Platform?

Security must be enforced at the resource level, not just the firewall level. API Platform integrates deeply with Symfony Security Voters, allowing you to apply granular access controls per operation.

Implementing Operation-Level Security

Add the security parameter to any operation attribute. This expression language syntax evaluates against the current user and the resource instance:

#[ApiResource(
    operations: [
        new Get(security: "is_granted('CASE_VIEW', object)"),
        new Put(security: "is_granted('CASE_EDIT', object)"),
        new Delete(security: "is_granted('ROLE_ADMIN')"),
    ]
)]

This pattern ensures that even if a user knows a resource ID, they cannot access it without passing the voter check. For multi-tenant systems serving multiple law firms or agencies, this is non-negotiable. Always test these expressions with PHPUnit integration tests rather than relying solely on manual QA.

Authentication Strategies

For most modern APIs in 2026, JWT authentication via lexik/jwt-authentication-bundle remains the standard. OAuth2 with Passport is appropriate only when acting as an identity provider. Avoid session-based auth for pure API projects unless serving a same-origin SPA exclusively. Configure stateless firewalls in security.yaml and ensure your token refresh mechanism handles clock skew gracefully for international clients.

HTTP RequestBearer TokenFirewallJWT DecodeVoter Checkis_granted()Data ProviderDoctrine Query403 Forbidden
Authorization flow in Symfony API Platform for REST and GraphQL demonstrating JWT validation before voter execution

When should you choose REST versus GraphQL in API Platform?

Supporting both protocols simultaneously is technically possible but strategically questionable for every endpoint. Understanding the trade-offs prevents over-engineering.

CriteriaREST EndpointGraphQL Endpoint
Caching StrategyHTTP Cache headers, CDN friendlyComplex, requires persisted queries
Client FlexibilityFixed shape, versionedDynamic field selection
File UploadsMultipart/form-data nativeRequires special mutation handling
Error HandlingStandard HTTP status codes200 OK with errors array
Learning CurveLow, universal toolingHigher, specialized clients

In practice, I recommend exposing REST as the primary interface for public APIs and third-party integrations. Reserve GraphQL for complex internal dashboards or mobile apps where bandwidth conservation outweighs caching benefits. For Nepal-based legal portals where internet connectivity varies, REST with aggressive HTTP caching often outperforms GraphQL's flexibility. If you are building admin panels specifically, consider whether Filament-style admin solutions might serve better than a raw GraphQL layer.

How do you optimize performance and handle pagination?

Default configurations rarely survive production load. API Platform’s pagination defaults to 30 items per page, which may be too high for complex joined queries or too low for data-heavy tables.

Configuring Efficient Pagination

Set sensible defaults globally in config/packages/api_platform.yaml and override per-resource where needed:

api_platform:
    defaults:
        pagination_items_per_page: 25
        pagination_maximum_items_per_page: 100
        pagination_client_items_per_page: true

Always enforce a maximum limit. Without this cap, a malicious or careless client can request ?itemsPerPage=10000 and crash your database. For large datasets, implement cursor-based pagination instead of offset-based to avoid performance degradation on deep pages. API Platform supports this natively via the PaginationType enum in your resource attributes.

Query Optimization and Eager Loading

N+1 problems are the silent killer of API performance. Use the eager fetch mode in Doctrine mappings or API Platform’s fetchEager option for frequently accessed relations. Monitor slow query logs religiously. On one high-traffic directory project, switching from lazy to eager loading on category relations reduced average response time from 450ms to 85ms. Pair this with Redis caching for collection endpoints that change infrequently.

Slow Collection Endpoint?Check N+1 QueriesEnable Eager FetchAdd Redis CacheCursor PaginationCache Tags Invalidation
Decision flowchart for optimizing Symfony API Platform for REST and GraphQL performance bottlenecks

What are the common pitfalls when upgrading to API Platform 4?

Upgrading from v3 to v4 in 2026 introduces breaking changes that catch many teams off guard. The most significant shift is the removal of legacy metadata formats. All YAML and XML resource configurations must be migrated to PHP attributes before upgrading. The official Rector ruleset automates about 80% of this conversion, but custom extensions require manual refactoring.

Another frequent issue involves the new state processor system replacing data persisters. If you have custom write logic, the old DataPersisterInterface will fail silently or throw deprecation warnings depending on your compatibility layer configuration. Rewrite these as State Processors immediately. Also verify your filter configurations; some query parameters changed naming conventions between versions, particularly around date range and nested property filtering.

Finally, audit your test suite. Functional tests that assert exact JSON structures may break due to reordered keys or added Hydra context fields. Use JSON schema validation in tests rather than exact string matching to make your suite resilient to minor spec-compliant changes. For teams managing multiple legacy PHP applications, coordinating these upgrades alongside server-level PHP version bumps requires careful planning as outlined in discussions about hiring experienced developers who understand both application and infrastructure layers.

Moving Forward with Production APIs

Symfony API Platform for REST and GraphQL offers unmatched developer productivity when configured correctly, but it demands respect for its opinionated architecture. Start with strict security voters, enforce pagination limits early, and resist the urge to enable GraphQL everywhere by default. Measure real-world performance before optimizing, and always validate your upgrade path against the official migration guides rather than blog tutorials that may lag behind the 2026 releases. If you need assistance architecting or auditing your API platform implementation, reach out to discuss your project requirements.

Frequently Asked Questions

A PHP framework built on Symfony for creating REST and GraphQL APIs using metadata-driven resource configuration.

Custom API projects typically range from NPR 300,000 to 800,000 (USD 2,250–6,000) depending on entity complexity and integrations.

Yes, it auto-generates REST endpoints and a GraphQL schema from the same PHP resource classes without code duplication.

Run composer require api-platform/core in your Symfony 7.x project root. Ensure PHP 8.2 or higher is active. The bundle auto-registers via Flex. Configure api_platform.yaml to define resource directories, enable GraphQL if needed, and set serialization groups. Clear cache with php bin/console cache:clear after installation to register new routes and metadata providers properly.

They solve different problems. Sanctum handles authentication for Laravel apps; API Platform is a full resource-centric framework generating CRUD, documentation, and validation automatically. For complex domain models requiring hypermedia, filtering, and OpenAPI specs out of the box, API Platform reduces boilerplate significantly. For simple token-authenticated endpoints within an existing Laravel app, Sanctum remains lighter. Choose based on whether you need a dedicated API framework versus an auth layer.

It integrates with Symfony Security components natively. Use JWT tokens via lexik/jwt-authentication-bundle for stateless REST/GraphQL auth. Define access_control rules in security.yaml and apply #[ApiResource] security attributes directly on entities for field-level permissions. I regularly configure voter-based authorization where business logic determines access beyond simple roles. Always validate permissions server-side; never trust frontend visibility flags alone when exposing sensitive legal or financial data through API endpoints.

Yes. Override operations using #[GetCollection], #[Post], or custom controller attributes while keeping auto-generated ones intact. Create custom state processors and providers implementing ProcessorInterface or ProviderInterface for complex business logic instead of modifying core generation. Use decoration patterns to extend default behavior. This approach survives framework upgrades because customization lives in application code, not vendor files. On production systems I maintain, this separation has prevented breaking changes across three major API Platform versions.

Apply #[ApiFilter] attributes to resource properties specifying filter classes like SearchFilter, RangeFilter, or OrderFilter. For complex queries, create custom filters implementing FilterInterface and register them as services. Combine multiple filters on single resources for faceted search. Configure parameter names and strategies per property. In my experience building directory platforms, combining SearchFilter with custom date-range filters provided sufficient query flexibility without exposing raw database queries or requiring separate search infrastructure.

N+1 queries are the most common issue. Eager fetch related entities using fetchEager: true in ApiResource or serialization groups to prevent lazy loading during collection requests. Add proper database indexes for filtered and sorted fields. Use pagination defaults (30 items) to avoid unbounded queries. Enable Doctrine query logging in dev to spot inefficiencies. On a legal portal I built, adding eager fetching and composite indexes reduced average collection response time from 800ms to under 120ms without caching.

It introspects PHP attributes, validation constraints, and serialization groups to produce OpenAPI 3.1 specs automatically at /docs.json. Customize descriptions using #[ApiProperty] and operation-level openapiContext attributes. Add authentication schemes via security configuration. The interactive Swagger UI mounts at /docs by default. Review generated docs critically; auto-generated schemas sometimes expose internal fields unintentionally. Explicitly configure serialization groups to control documented output rather than relying on default exposure behavior.

Yes, but requires manual configuration. Create dedicated upload endpoints with custom state processors handling file validation, storage, and entity association. Use VichUploaderBundle or Spatie Media Library equivalents for storage abstraction. Never expose direct filesystem paths through API responses. Validate MIME types and sizes server-side before processing. On client projects involving document workflows, I implement separate upload resources returning signed URLs or media identifiers rather than embedding binary data in JSON responses to keep payloads manageable.

Use URL path versioning (/v1/resources) via route_prefix in ApiResource or header-based versioning with Accept-Version headers. Maintain parallel resource classes per version rather than conditional logic within single classes. Deprecate old versions using deprecated: true attribute while serving migration notices in response headers. Support multiple versions during transition periods. Breaking changes warrant new versions; additive changes can extend existing versions. Document version lifecycle clearly in OpenAPI specs so consumers plan migrations proactively.

Write functional tests extending ApiTestCase for endpoint integration coverage. Test HTTP methods, status codes, serialization output, and authorization rules against test database fixtures. Use PHPUnit assertions for JSON structure validation. Mock external services but test real database interactions. Supplement with unit tests for custom state processors and validators. I run these in GitLab CI pipelines before every deploy. Aim for coverage of all public operations; internal helpers need less rigorous testing. Regression tests prevent accidental contract breaks during refactors.

Use Deployer 7 with zero-downtime symlinked releases on Ubuntu servers. Build frontend assets locally or in CI since production servers should not run Node. Pre-warm OPcache after deployment via php-fpm reload. Configure shared .env and var/cache directories between releases. Set up health check endpoints monitoring database connectivity and critical dependencies. Run doctrine:migrations:migrate during deploy hooks. On sister sites sharing deployment infrastructure, this pattern has delivered consistent rollbacks and predictable release cycles without downtime for API consumers.

Skip it for simple internal APIs with fewer than five endpoints where manual controllers suffice. Avoid when team lacks Symfony expertise and timeline is tight; learning curve is real. Don't force it onto legacy monoliths needing incremental modernization; strangle gradually instead. If requirements are purely GraphQL without REST, consider dedicated GraphQL libraries. API Platform shines for resource-heavy domains with standardized CRUD patterns. For bespoke workflows with minimal entity mapping, traditional Symfony controllers often deliver faster with less abstraction overhead.

Share this article

Quick Contact Options
Choose how you want to connect me: