Why OData?
Every Laravel app that grows a frontend grows an API. And every one of them arrives at the same handful of problems: how does the client ask for fewer columns, more rows, a different sort order, a filtered subset, a related record in the same round trip?
The question was never "REST or not". It is who writes the query language. Today, that is you. For every project, every time.
This page is the argument for letting a standard write it instead. It also names the cases where that is the wrong call.
What REST actually says — and what it doesn't
REST is an architectural style, not a specification. Roy Fielding's constraints give you resources identified by URIs, a uniform interface over HTTP verbs, statelessness, and cacheability. That is a good foundation and it is genuinely all REST claims to be.
What REST does not define:
- Projection — how a client asks for three of forty columns.
- Filtering — how a client expresses
price > 10 and active eq true. - Sorting and paging — the parameter names, the envelope, the cursor semantics.
- Relations — how a client pulls a parent and its children in one request.
- A schema — anything machine-readable that describes what the endpoint returns.
Five gaps. Every team fills them privately, and then documents them privately. The API works. The contract lives in a wiki that starts drifting in week three.
How a Laravel team fills those gaps today
| Gap | Typical Laravel answer | What it costs |
|---|---|---|
| Projection | API Resources, or a hand-rolled ?fields= | A convention only your team knows |
| Filtering | Query scopes per endpoint, or spatie/laravel-query-builder | Filter logic spread across controllers |
| Sorting | An ?sort= param you invented | Whitelisting done by hand, or not at all |
| Paging | paginate() | Laravel's envelope, not a portable one |
| Relations | with() plus an ?include= param | Eager-loading rules encoded in the controller |
| Schema | Scribe or hand-written OpenAPI | Maintained separately from the code that serves the data |
None of this is bad engineering. It is simply work that recurs on every project and produces an asset that is worthless outside it.
The options on the table
Hand-rolled REST with Eloquent API Resources. The default, and it got better: Laravel 13 ships first-party JSON:API Resources with sparse fieldsets, includes, links, and meta. That closes the shape gap without a package. It does not close the query gap, and it publishes no schema. Choose it when the API serves one frontend you also own, and the surface is small.
spatie/laravel-query-builder. The closest thing in the Laravel world to a query language: allowed filters, sorts, includes, declared per endpoint. Excellent package. It is a convention, not a contract — the vocabulary is per-project and nothing consumes it automatically. Choose it when you want filtering discipline without adopting a protocol.
JSON:API. A real specification, and a good one, for the envelope: document structure, resource identity, relationships, errors. Filtering is explicitly left implementation-defined. So the shape is standard and the query is still yours. Choose it when you have many clients that need a predictable response format more than a predictable query syntax.
GraphQL (Lighthouse). A genuine query language with a genuine schema, and the strongest answer here for over-fetching across many diverse clients. In exchange you own the whole client story, plus pagination policy, N+1 control, and a complexity budget. There is no generic consumer that points at a GraphQL endpoint and renders a working table. Choose it when many teams with different data needs consume one backend.
API Platform for Laravel. Exposes Eloquent models as REST (JSON-LD, JSON:API, HAL) and GraphQL from one model, with a substantial framework of its own. Choose it when you want that breadth and are willing to adopt its model.
OData. A specified URL query language, a machine-readable schema at a well-known address, and an existing population of clients that already speak it. Choose it when the consumer is a data-aware client — a UI5 table, Excel, a BI tool, another enterprise system — or when the API contract has to outlive the team that wrote it.
Side by side
| Query language | Machine-readable schema | Generic clients | Carries semantics | Familiar to a new hire | |
|---|---|---|---|---|---|
| Hand-rolled REST | no | separate document | no | no | yes |
spatie/query-builder | per project | no | no | no | yes |
| JSON:API | undefined | no | no | no | mostly |
| GraphQL | yes | yes | no | types only | mostly |
| API Platform | partly | yes (OpenAPI) | no | types only | less so |
| OData | yes | yes ($metadata) | yes | yes | no |
The last column is where OData loses, and it is a real cost. The rest of this page is the argument that the other five columns are worth it — and section When not to use OData is where they are not.
What OData adds
OData is to REST APIs what SQL is to databases: a standard query language any client can speak without custom documentation. Four things follow from that.
A query language that is specified. $filter, $select, $orderby, $expand, $top, $skip, $count, $search, $compute. Not parameter names your team agreed on in a standup. Names with a grammar, an ABNF, and a conformance level.
A schema clients can compile. Every service publishes $metadata: entity types, properties, keys, relationships, functions. It is generated from the same model that serves the data, so it cannot drift. The documentation problem is not solved by discipline here. It is solved by construction.
Clients that already exist. SAP Fiori Elements builds tables, filter bars and value helps from $metadata alone. Excel Power Query and Power BI consume an OData feed natively. Salesforce Connect maps external objects onto it. Dynamics 365 and SAP S/4HANA speak it as their primary API protocol.
Semantics, not just types. This is the part nothing else on the list offers. Annotations attach meaning to the model: this string is a label, this decimal is a currency amount, this property has a value help, this field is a unit of measure. GraphQL's schema describes shapes. OData's describes shapes and what they mean, which is why the client can render a form nobody designed.
Standing. OData v4.01 has been an OASIS Standard since April 2020. OData v4.0 Core and its JSON Format are published as ISO/IEC 20802-1:2016 and ISO/IEC 20802-2:2016. If an architecture board asks what your API conforms to, that is a defensible answer.
"Isn't OData an SAP thing?"
It is fair to ask. OData carries an enterprise reputation, the XML metadata document is verbose, and most JavaScript developers have never touched it.
Two of those are true and one is a misreading. OData was designed at Microsoft, standardized at OASIS, and published by ISO. SAP is its most visible adopter, not its owner. The metadata document is verbose, and CSDL also has a JSON representation if the XML bothers you — but either way the server writes it once and every client cashes it in. And the unfamiliarity is real, which is exactly why it belongs in the decision rather than in a footnote.
Do's
- Model the read surface. Don't mirror the database. An entity set is an API contract, not a table dump. Name properties for consumers.
- Let the protocol carry the query. Once
$filteris doing the work, controllers stop growing filter branches. Resist adding a custom parameter next to a system query option. - Treat
$metadataas the contract. It is a file. Diff it in CI. A surprise change there is a breaking change for someone. - Bound every result set. Configure a maximum page size and let clients negotiate downward with
Prefer: odata.maxpagesize. An unbounded collection is an outage waiting for a Monday. - Annotate. Labels, units, value helps. Every annotation you add is a UI someone else does not have to hand-build.
- Keep reads read-only. Give writes their own explicit, validated endpoints where the business rules can live.
Don'ts
- Don't hand-roll a mini-OData. A
$filter-lookalike parsed with a regular expression is the worst of both worlds: the syntax of a standard without its guarantees. - Don't pass client input into
whereRaw. Expose modeled properties and let the engine build the query. Filtering is an attack surface first and a feature second. - Don't expose everything just because discovery made it cheap. Model discovery is a convenience, not a policy. Internal columns, tenancy keys, and personal data need a decision each.
- Don't use OData as an RPC transport. "Approve this invoice" is not a
$filter. Business operations deserve endpoints that say what they do. - Don't version by URL churn. Extend the schema additively. New properties and new entity sets don't break clients that never asked for them; renames do.
- Don't pick OData for consumers who have no OData tooling. The ecosystem argument only pays if the ecosystem is on the other end of the wire.
When not to use OData
Be honest about the fit. Reach for something else when:
- The API is a public consumer API whose audience is application developers with no OData tooling. Their learning curve is your adoption curve.
- You are building a mobile-first BFF, where a purpose-shaped payload per screen beats a general query surface.
- The domain is write-heavy or RPC-shaped, and reads are incidental.
- The app has one endpoint feeding one page. A protocol is overhead until there is something to standardize.
OData earns its keep where data is queried by clients you do not control, or by clients that build themselves from a schema. That is a large and well-defined space. It is not every space.
Where this engine stands
laravelui5/odata implements OData v4 for Laravel, built against the v4.01 specification texts. It is:
- Read-only by design. The engine serves queries. Writes stay in your application, where validation and business rules belong. In the wider LUX stack, they are Ui5Actions.
- Streaming, not hydrating. Result rows stream as SQL records rather than being hydrated into Eloquent models, which is where the performance comes from on large collections.
- Pre-compiled.
php artisan odata:cachecompiles the EDM to PHP classes, so no schema discovery happens at request time. - MIT. No account, no gate, no upsell in the way.
Next
- OData V4 Concepts — the vocabulary: entity types, entity sets, navigation properties, annotations.
- Installation — requirements and configuration.
- Quickstart — an Eloquent model on the wire in five minutes.