API Platform or Hand-Rolled REST: A Decision Framework

A Symfony decision framework for choosing API Platform or hand-rolled REST based on delivery speed, customization, performance, and ownership cost.

Brass scale balancing API Platform on one pan with a gear and api badge against hand-rolled REST on the other pan with a Symfony Route attribute code snippet, alongside a decision framework matrix listing time to market, flexibility, maintainability, performance, team skills, long-term cost and business complexity

The question I get asked most often by Symfony teams that are about to start a new API is some variation of: “Should we use API Platform, or just write the controllers ourselves?”

It is the wrong question, slightly. The right question is: “On which dimensions are we willing to trade short-term velocity for long-term flexibility, and on which dimensions are we willing to trade long-term flexibility for short-term velocity?”

API Platform makes a specific trade. It gives you, for free, an enormous amount of behavior that would otherwise be code you write and maintain. In exchange, it commits you to a particular way of thinking about resources, operations, and serialization, and it makes some kinds of customization easy and other kinds painful.

Hand-rolled REST makes the opposite trade. You write everything. You also control everything. There is no learning curve beyond Symfony itself, and there is no library upgrade to manage that might change the shape of your API.

Neither answer is universally right. The decision depends on which dimensions matter most for your specific application, your specific team, and your specific timeline. This essay is a framework for making that decision honestly.

The seven dimensions

I have built APIs both ways for enough years to have settled on seven dimensions that actually move the needle. Score your project on each, and the answer will usually fall out.

1. Time to market

If you need a working CRUD API for a domain entity, deployed to production, in less than a week, API Platform is hard to beat.

A typical API Platform resource:

PHP
namespace App\ApiResource;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Delete;

#[ApiResource(
    operations: [
        new Get(),
        new GetCollection(),
        new Post(),
        new Patch(),
        new Delete(),
    ],
    paginationItemsPerPage: 20,
)]
class Article
{
    public string $title;
    public string $body;
    public \DateTimeImmutable $publishedAt;
}

That is a full REST API: list with pagination, get one, create, partial update, delete. With Hydra documentation, OpenAPI spec generation, content negotiation across JSON-LD, JSON, and XML. Configurable filters. Validation rules from Symfony’s validator. All without writing a single controller.

Hand-rolling the same surface in raw Symfony is several days of work, even with a generator. Not because any one piece is hard, but because the cumulative work of pagination, validation, error responses, OpenAPI documentation, and consistent JSON shapes adds up.

API Platform wins this dimension by a wide margin. If your project has a tight deadline and the API surface is mostly resource-oriented, this alone may decide the choice.

2. Resource fit

API Platform is designed around the idea that your API exposes resources, and that those resources map cleanly onto entities (or DTOs that look like entities). The closer your domain matches that shape, the better the fit.

The shape API Platform fits well:

  • Entities with attributes, relations, validation rules.
  • Operations that are mostly Create, Read, Update, Delete on those entities.
  • Filters that are mostly equality, range, or text search on the entity fields.

The shape API Platform fits badly:

  • Operations that are commands, not resource manipulations. “Cancel this order” is not a PATCH on an order; it is a command. You can model it as a state mutation, but you fight the framework.
  • APIs where the response shape is the result of a computation, not a serialized entity. Aggregates, dashboards, reports.
  • APIs where the same logical resource has very different shapes at different endpoints. A User that returns 4 fields on the public endpoint and 47 fields on the admin endpoint, with a different security model on each.

You can do all of these things in API Platform with custom operations, custom data providers, custom output classes. But once you are doing all that, you have written a hand-rolled REST API on top of an API Platform shell, and the shell is paying for itself less and less.

Score this dimension by the percentage of your endpoints that fit the resource model. If it is over 70%, API Platform is probably the right call. If it is under 40%, hand-roll.

3. Customization rate

This is the dimension most teams get wrong. They project their current customization needs forward and assume they will stay constant. They almost never do.

The pattern is consistent: a project starts with mostly standard CRUD. After 18 months, the team has 30-50 custom operations, 20 custom normalizers, 15 custom filters, and a directory of API Platform extensions that nobody fully understands. Each was the right call individually. Together they form a layer of customization that is harder to maintain than the controllers it replaced.

A useful question: of the API endpoints currently in production, how many of them work with the framework’s defaults, and how many require custom code?

PHP
// Default API Platform behavior: fine
#[ApiResource(operations: [new Get(), new GetCollection()])]

// Customized: manageable, but starting to leak
#[ApiResource(
    operations: [
        new Get(
            normalizationContext: ['groups' => ['article:read']],
            security: "is_granted('ROLE_USER') and object.author == user",
            provider: ArticleProvider::class,
            processor: ArticleProcessor::class,
        ),
    ],
)]

Each piece of customization is a place where the team needs to know API Platform internals. Custom data providers, custom processors, custom event subscribers on KernelEvents::REQUEST to mutate the API Platform context. After a few of these, the team is effectively maintaining a fork of API Platform’s behavior in the form of an extensions directory.

If your API has more than a handful of operations that need significant deviation from defaults, that is a signal toward hand-rolled. Not because API Platform cannot do it, but because the cost of doing it within API Platform exceeds the cost of doing it in plain Symfony.

4. Documentation needs

API Platform generates an OpenAPI spec, a Hydra documentation, a Swagger UI, a ReDoc UI, and (with the right bundles) a GraphQL playground, all from your resource definitions. The documentation stays in sync with the code because it is the code.

Hand-rolled REST means you write the OpenAPI spec separately, either by hand (which drifts) or with annotations like nelmio/api-doc-bundle (which mostly works but adds its own annotations to maintain).

If your API is consumed by external developers, partners, or a mobile team that you do not see every day, the documentation gap matters a lot. Auto-generated docs that are always accurate are a real gift.

If your API is consumed only by your own front-end team that sits next to you, the gap matters less. You can show them the controller.

Score this dimension by audience. External or distributed: heavy weight on API Platform. Internal and adjacent: weak signal.

5. Performance characteristics

API Platform’s default request handling has more steps than a hand-rolled controller: routing, deserialization, denormalization, validation, the operation logic, normalization, serialization, the documentation hooks. Each step is fast, but there is more of them.

For most APIs this is invisible. A typical API Platform endpoint adds 10-30ms to request time compared to a hand-rolled equivalent. On a non-critical internal API, this is noise.

For some APIs it is not invisible. Specifically:

  • High-volume endpoints (thousands of requests per second).
  • Endpoints with very large response payloads where serialization dominates.
  • Endpoints behind a hard SLA (sub-50ms p99).

For these, hand-rolled gives you direct control over what gets done per request. You can skip serialization steps, return raw JSON strings, pre-compute responses. None of which are impossible in API Platform, but all of which fight the framework.

This is the dimension where teams over-weight performance and under-weight everything else. Most APIs do not have a 50ms SLA. Most APIs are fine with a 100ms median. Be honest about your actual performance requirements before letting this dimension swing the decision.

6. Team skills and continuity

API Platform is its own framework on top of Symfony. To use it well, the team needs to learn:

  • The metadata model (ApiResource, Get, Post, Patch, etc.)
  • The state pattern (providers and processors)
  • The serialization pipeline (normalizers, denormalizers, contexts, groups)
  • The extension system (filter extensions, query extensions, security extensions)
  • How API Platform interacts with Symfony’s own components (security, validation, messenger)

This is real cost. A new engineer joining the team spends weeks learning API Platform conventions on top of Symfony. The depth of understanding needed to debug a serialization issue is significantly more than the depth needed to debug a hand-rolled controller.

In exchange, the team that learns API Platform well becomes very productive in it. New endpoints take minutes. Standard customizations become idiom.

Be honest about whether your team will invest in learning the framework. If they will, the investment pays back fast. If they will not, you end up with a half-learned API Platform application where the team uses 20% of the framework and writes around the rest, which is the worst of both worlds.

7. Long-term ownership

APIs are long-lived. The decision you make today commits you to a maintenance posture for as long as the API has consumers, which is usually longer than you expect.

API Platform’s long-term ownership profile:

  • Major version upgrades happen every 1-2 years and historically have included some breaking changes. The team needs to budget for those.
  • The library is healthy and well-maintained, with active contributors and clear roadmap.
  • The bus factor is acceptable but not zero. If the maintainers walked away, you have a fork problem.
  • The conventions API Platform encodes (HATEOAS, JSON-LD, Hydra) are stable but not universal. Some consumers will not understand them and will need configured plain-JSON output.

Hand-rolled REST’s long-term ownership profile:

  • No upgrade risk beyond Symfony itself.
  • No external maintainer dependency.
  • Every line of behavior is your team’s to maintain. This includes things you did not expect to maintain (pagination edge cases, OpenAPI spec drift, error envelope consistency).
  • The conventions are whatever your team agreed to, which means they are also whatever your team forgot to agree to. A hand-rolled API tends to develop inconsistency over time as different developers make different choices.

For an API expected to live more than three years and serve external consumers, both options have ownership cost; they are just different costs. Pick the one your team is better positioned to absorb.

The decision matrix

Score your project on each dimension. Pick the option that wins more weighted dimensions, weighting the ones that matter most for your context.

Dimension API Platform wins when Hand-rolled wins when
Time to market Tight deadline, mostly CRUD No deadline pressure, want full control
Resource fit >70% endpoints fit resource model <40% endpoints fit resource model
Customization rate Mostly defaults work Many endpoints need significant deviation
Documentation External or distributed consumers Internal, adjacent consumers
Performance No sub-50ms SLA Hot path, large payloads, hard SLAs
Team skills Team will invest in learning the framework Team will not, or has high turnover
Long-term ownership Comfortable with framework upgrades Want minimal external dependencies

The honest summary: API Platform is the right answer for most internal CRUD-shaped APIs in established Symfony shops. Hand-rolled is the right answer for performance-critical APIs, command-shaped APIs, or projects where the team will not invest in learning a second framework.

Worked examples

To make this concrete, three real shapes of project and which I would choose for each.

Project A: An admin panel API for an internal SaaS

  • 40 entities, mostly straightforward CRUD.
  • Consumed by one frontend team that sits in the same room.
  • Performance budget: a few hundred milliseconds is fine.
  • Team has experience with API Platform.

Choice: API Platform. This is the canonical fit. The team will write 40 resources in two days and spend the rest of the project on actual business logic, not pagination plumbing.

Project B: A public mobile app backend with payment flows

  • 25 endpoints, but mostly command-shaped (place order, cancel order, dispute charge, request refund).
  • Strict SLAs on the order placement path (sub-100ms p99).
  • Consumed by an external mobile team that will write integration code against an OpenAPI spec.
  • Team has Symfony experience but not API Platform experience.

Choice: Hand-rolled, with nelmio/api-doc-bundle for the OpenAPI generation. The command shape, the performance budget, and the unfamiliar framework all push away from API Platform. The OpenAPI need can be served by Nelmio with controlled annotations.

Project C: A multi-tenant headless CMS API

  • 60 entities, all CRUD-shaped.
  • Consumed by an unknown number of external developers building custom frontends.
  • Performance budget: 200ms p95 is acceptable.
  • Team is small and stable.

Choice: API Platform. The combination of resource shape, large entity count, and external developers needing OpenAPI documentation makes API Platform very productive. The team can invest in learning it because they will not be replaced, and the framework will pay back across all 60 entities.

What teams get wrong

Three patterns I see consistently:

Picking API Platform because it is the fashionable answer. It is the fashionable answer in some Symfony circles, and that is not a reason. If your project is mostly command-shaped, hand-roll. If your team will not invest, hand-roll. The framework’s popularity in the Symfony community is not, by itself, a sufficient reason to commit to its conventions.

Picking hand-rolled because the team distrusts magic. Some teams have a cultural preference for explicit code that they can read line by line. This is a reasonable preference, but it should be examined: hand-rolled REST that re-implements pagination, validation, error envelopes, and OpenAPI generation is not less magic, it is just more code. If the magic is what makes API Platform work, the absence of the magic does not actually buy you simplicity, just volume.

Picking one and refusing to mix. Both options can coexist in the same application. A typical pattern: API Platform for the 70% of CRUD endpoints, hand-rolled controllers for the command-shaped or performance-critical ones, mounted under different route prefixes. The team that commits dogmatically to one or the other usually ends up fighting the framework on whichever endpoints do not fit.

When neither is the right answer

Some APIs do not want to be REST at all. The decision framework above assumes you have already chosen REST. If you have not:

  • GraphQL is the right answer when consumers need to specify the shape of the response and the API has many possible projections. API Platform supports GraphQL, but a dedicated GraphQL stack (webonyx/graphql-php with overblog/GraphQLBundle) often fits better.
  • JSON-RPC is the right answer when the API is mostly command-shaped and the resource framing is forced. A small set of named operations with typed inputs and outputs is closer to what the application actually does.
  • gRPC is the right answer when the consumers are other backend services and the contract benefits from strict schema enforcement. Symfony does not have first-class gRPC support, but it can be added with reasonable effort.

The REST decision is not the only decision. Choose the protocol shape before you choose the implementation strategy.

A short version

If you read nothing else: API Platform for resource-shaped, CRUD-heavy, externally-documented APIs in teams that will invest in the framework. Hand-rolled for command-shaped, performance-critical, internally-consumed APIs in teams that will not. Mix freely when the application has both shapes. Do not let the framework choice become a religious commitment.


If you are starting a new API and want help making this decision in the context of your specific project, my business alignment engagement includes a one-week API decision sprint that scores your requirements against this framework and produces a costed implementation plan for whichever direction makes sense.

References

Ready to Fix Your Architecture?

Book a free 30-minute call with Silas. No sales pitch, just a direct conversation about your challenges.

Typically responds within 24 hours.

Book a Free Call