REST API Development – Beyond CRUD

REST is everywhere – but most REST APIs are just database tables exposed over HTTP. GET, POST, PUT, DELETE mapped to CRUD, with all the business logic leaking into controllers or, worse, left to the frontend to figure out. After years of building web APIs in PHP/Symfony and TypeScript/Node.js, I design APIs that express business intent, enforce domain rules, and guide the client through available actions – not just serve data.

REST is CRUD-oriented. Your domain is not.

The standard REST vocabulary – GET, POST, PUT, DELETE – maps naturally to create/read/update/delete. That works for simple resources. But real business operations rarely fit into “update a record”.

Consider an order management system. The client doesn’t “update” an order – they confirm it, cancel it, request a refund, assign a carrier. Each of these is a distinct use case with its own preconditions, business rules and side effects. Hiding all of them behind a single PUT /orders/{id} is losing information – and pushing decision-making to the caller.

PATCH as a use case trigger

One approach I use to bridge REST conventions with clean architecture is to leverage PATCH to trigger specific use cases. Rather than a generic “update”, each PATCH request targets a named operation:

PATCH-driven use case dispatch PATCH /orders/42/confirm → ConfirmOrderHandler PATCH /orders/42/cancel → CancelOrderHandler PATCH /orders/42/assign-carrier → AssignCarrierHandler

Each route maps to a dedicated command handler in the application layer. The controller is a thin adapter: it deserializes the request, builds the command, dispatches it. No business logic in the controller. No ambiguity about what the caller intended.

Is this “pure” REST? Purists might argue. But REST was never about purity – it was about using HTTP semantics to make intent clear. A PATCH that modifies part of a resource’s state through a well-defined business operation is more expressive than a PUT that replaces the entire resource representation and hopes the server can figure out what changed.

CQRS – separating commands from queries at the API level

The approach described above naturally leads to CQRS (Command Query Responsibility Segregation). When your API routes explicitly distinguish between “things that read” and “things that change state”, the architecture becomes clearer at every layer.

Queries – read models optimized for the client

GET endpoints return read-optimized projections, not raw entity dumps. The query side can use denormalized views, Elasticsearch indexes, or dedicated DTOs – whatever serves the consumer best. No Doctrine lazy-loading surprises. No N+1 queries. The read model is designed for the screen, not for the ORM.

Commands – explicit intent, explicit side effects

POST and PATCH endpoints dispatch commands to dedicated handlers. Each handler validates preconditions, applies business rules, and may emit domain events (order confirmed, carrier assigned, invoice generated). The command handler is where the domain lives – not the controller, not the ORM.

In a Symfony context, the Messenger component provides a natural command bus. Commands are simple value objects, handlers are services. The dispatcher doesn’t care if the command came from an HTTP request, a CLI command or an async message – which is exactly the point of clean architecture.

HATEOAS – the API tells the client what it can do

One of the most underused principles in REST API design is HATEOAS (Hypermedia As The Engine Of Application State). The idea is simple: the API response includes links that describe what actions are available given the current state of the resource.

API response with hypermedia links { "id": 42, "status": "pending", "total": 189.90, "_links": { "self": { "href": "/orders/42" }, "confirm": { "href": "/orders/42/confirm", "method": "PATCH" }, "cancel": { "href": "/orders/42/cancel", "method": "PATCH" } } }

Once the order is confirmed, the confirm link disappears and new links appear (assign-carrier, request-refund). The frontend doesn’t need to know the business rules that determine which transitions are valid – the API tells it.

Why this matters for frontend teams

Without HATEOAS, the frontend duplicates domain logic: “if status is pending and payment is confirmed and the user has the manager role, then show the confirm button”. This logic is fragile, scattered across components, and drifts from the backend rules over time.

With HATEOAS links, the frontend logic becomes: “if the link exists, show the button”. The complexity stays where it belongs – in the domain layer of the backend. The frontend becomes a thin rendering layer that follows the API’s instructions.

This is especially valuable in complex B2B applications where business rules evolve frequently. A rule change on the backend automatically adjusts what the frontend can display – without redeploying the frontend, without coordinating releases.

API design principles I apply

Contract-first design

Define the API contract (OpenAPI / JSON Schema) before writing code. The contract is the shared language between backend and frontend teams. It drives code generation, documentation, and automated testing.

Meaningful HTTP status codes

201 Created for resource creation. 409 Conflict for business rule violations. 422 Unprocessable Entity for validation errors. Not everything is a 200 or a 500.

Versioning strategy

URL-based versioning (/v1/) for public APIs, header-based for internal APIs. Deprecation policies with sunset headers and migration guides – not silent breaking changes.

Pagination, filtering, sorting

Cursor-based pagination for large datasets. Consistent filtering conventions. No surprise query parameters – every filter documented in the OpenAPI spec.

Error responses as first-class citizens

Structured error payloads with machine-readable codes, human-readable messages, and field-level validation details. The client can always programmatically handle the error, not just show a generic message.

Security by design

JWT authentication, OAuth2 flows, rate limiting, input validation at the boundary. OWASP API Security Top 10 as a baseline – not an afterthought.

A word on API Platform

API Platform is a powerful tool for generating REST and GraphQL APIs from Symfony entities with minimal code. For straightforward CRUD APIs, it delivers fast results: pagination, filters, OpenAPI documentation – all out of the box.

But when your API needs to express business operations beyond CRUD – when you need CQRS, custom state transitions, HATEOAS-driven workflows – API Platform’s abstractions (State Providers, Processors) start to constrain rather than accelerate. The tool works best when your domain happens to be CRUD-shaped. When it’s not, you spend more time working around the framework than coding the domain.

I’ve used API Platform on multiple projects (notably at IAD, where I detail the experience). My take: use it when the CRUD fit is genuine. For use-case-driven APIs with rich business logic, standard Symfony controllers with a command bus give you more control with less friction.

Tech stack for API development

Clients who trusted me

Sanofi
IAD Immobilier
Leboncoin
Air France

Let’s design your API

← Back to home