Motivation
Several existing approaches exist for serving TYPO3 content as structured data — Extbase repositories, the Record API, EXT:headless, and annotation-driven frameworks like EXT:t3api and EXT:nnrestapi. TCA_API was built because none of them solve the read-heavy API use case without significant trade-offs in performance, boilerplate, or flexibility.
Attention
The analysis below is based on code reading and architectural reasoning. Concrete, real-world benchmarks against production setups of EXT:t3api and EXT:nnrestapi have not yet been conducted. The numbers reflect the theoretical query-count differences between the strategies. Conclusions may not hold for all workloads.
Feedback and benchmark contributions from developers experienced with these extensions are very welcome — see Call for feedback at the end of this chapter.
Why existing approaches fall short
Why not EXT:nnrestapi?
EXT:nnrestapi is an
endpoint framework: you write a PHP class extending AbstractApi, annotate
its methods, and return data however you choose. This gives maximum flexibility,
but shifts all responsibility to the developer:
- No built-in relation loading. Returning Extbase domain objects hands serialization to TYPO3's standard DataMapper, which resolves every relation property individually during JSON conversion. A 20-item collection with 2 relation types produces the same 41 queries as any Extbase-based approach.
- No built-in filtering, pagination, or validation. Each endpoint is custom PHP. Adding a filter means writing a query constraint by hand; pagination requires manually counting rows and slicing results. Every resource needs its own implementation.
- No configuration model. There is no declarative description of a resource's shape, access rules, or allowed operations. Everything is imperative code inside action methods, growing linearly with the number of resources.
- Plain JSON output. Responses are plain JSON — no Hydra JSON-LD, no
@context, no@type, no discoverable collection links.
nnrestapi is a reasonable choice for bespoke, one-off endpoints where the flexibility is genuinely needed. It is a poor fit for exposing multiple resources uniformly.
Why not EXT:t3api?
EXT:t3api is the closest prior art — Hydra JSON-LD output, built-in filtering, pagination, serialization groups, and an API-Platform-inspired annotation model. It is a mature extension. The core limitation is the persistence layer:
- Built on Extbase DataMapper. Resources must be Extbase domain models
(
AbstractDomainObjectsubclasses). Queries usePersistenceManagerInterface::createQueryForType(), and all relation properties are resolved via Extbase'sDataMapper. - N+1 queries for embedded relations. DataMapper resolves relations through
LazyLoadingProxyobjects fetched on first access. Serializing 20 articles with 2 embedded relation types fires 41 database queries — one for the collection plus one per relation per row. - Extbase model required per resource. Exposing a table from a third-party extension that ships no domain model class requires creating one.
Why not the TYPO3 Record API?
The Record API
introduced in TYPO3 v13 provides RecordFactory and typed Record objects
with lazy relation resolution. It is a solid foundation for Fluid templates,
but has key limitations for API use:
- Per-record hydration overhead.
RecordFactory::createResolvedRecordFromDatabaseRow()instantiates aRecordobject per row, transforms each field throughRecordFieldTransformer, and wraps relations inLazyRecordCollectionorRecordPropertyClosureclosures. For a collection of 20 records with 5 relation columns, this creates 20 Record objects + 100 lazy wrappers — before any relation is even accessed. - No batch relation loading. When serializing a collection to JSON, every
lazy relation fires a separate query on first access. 20 articles × (1 color
+ 1 category MM) = 41 queries. The
GreedyDatabaseBackendmitigates this by pre-fetching an entire foreign table by PID, but this over-fetches and only helps within a single page context. - Designed for rendering, not serialization. Calling
$record->toArray()force-instantiates all lazy closures. There is no depth control, no cycle detection, and no way to configure which relations to embed vs. return as references.
Why not Extbase alone?
Extbase's DataMapper suffers from the classic N+1 query problem. Each
relation property on each domain object triggers a separate
getPreparedQuery() call. The @Lazy annotation defers queries but doesn't
batch them.
Why not EXT:headless?
EXT:headless replaces TYPO3's
HTML output with JSON via TypoScript JSON content objects. It uses the same
rendering pipeline (CONTENT cObjects, DataProcessors) and executes the same
queries as a normal page render. The benefit is smaller payloads for the
frontend, not fewer database queries.
How TCA_API solves this
TCA_API takes a fundamentally different approach: raw SQL via QueryBuilder with bulk preloading and a zero-boilerplate configuration model.
- No ORM, no object hydration. Records are raw associative arrays from
ConnectionPool::getQueryBuilderForTable(). Zero overhead from property mapping, proxy objects, or lazy wrappers. -
EmbedPreloader eliminates N+1 queries. Before serialization, the preloader scans all rows in a collection, collects every referenced foreign key, and executes one query per relation type — regardless of collection size:
- hasOne FKs:
SELECT * FROM colors WHERE uid IN (1, 2, 3) - hasMany MM:
SELECT f.*, mm.uid_local FROM categories f JOIN mm ON ... WHERE mm.uid_foreign IN (...) - hasMany foreignField:
SELECT * FROM children WHERE parent_id IN (...)
- hasOne FKs:
- Fixed query count. The number of queries is
1 + R(one collection query + one per relation type), not1 + N×R. Adding more rows to a page does not increase the query count. - Zero boilerplate per resource. A three-key PHP array is a complete resource definition — no domain model class, no repository, no controller, no routing config. Filtering, pagination, access control, and validation are declared in the same file.
Query count analysis
The query-count difference between strategies is deterministic and can be derived from simple formulas:
- Naive N+1 / Extbase / t3api:
Q = 1 + N × R - TCA_API (EmbedPreloader):
Q = 1 + R
Where N is the collection size and R is the number of embedded
relation types per record.
| Collection size | Relations | N+1 queries | TCA_API queries | Savings |
|---|---|---|---|---|
| 20 items | 2 | 41 | 3 | 92.7% |
| 50 items | 2 | 101 | 3 | 97.0% |
| 100 items | 3 | 301 | 4 | 98.7% |
| 100 items | 5 | 501 | 6 | 98.8% |
These numbers are theoretical projections from the formula, not measurements from live extensions. Wall-clock impact depends on database round-trip latency, query complexity, and caching — all of which vary significantly between projects.
Comparison matrix
| Concern | TCA_API | EXT:t3api | EXT:nnrestapi | Record API | EXT:headless |
|---|---|---|---|---|---|
| Query strategy | Bulk preload | Extbase N+1 | Extbase N+1 or manual | Lazy + greedy-by-PID | Same as page render |
| Queries (20 × 2 rels) | 3 | 41 | 41 (or raw, no rels) | 41 (¹) | 40-80 (²) |
| Object overhead | None (raw arrays) | Domain objects + proxies | Domain objects or arrays | Record + closures | TypoScript cObjects |
| Configuration model | PHP array (zero code) | Annotations on model | Per-method PHP | N/A | TypoScript |
| Filtering + pagination | Built-in | Built-in | Manual per endpoint | N/A | N/A |
| JSON output format | Hydra JSON-LD | Hydra JSON-LD | Plain JSON | Manual | Native JSON |
| Extbase model required | No | Yes | Optional | No | No |
| Write operations | DataHandler | Repository | Manual | N/A | N/A |
(¹) The Record API's GreedyDatabaseBackend can reduce query count when related records share the same PID, so actual numbers may be lower in single-page contexts.
(²) EXT:headless query counts vary widely depending on TypoScript setup and DataProcessor configuration; the range reflects typical page-render workloads.
Call for feedback
The comparison above is based on source-code reading and architectural reasoning. It has not been independently verified by developers experienced with EXT:t3api or EXT:nnrestapi, nor has it been validated against real-world production workloads.
If you work with these extensions and find that any claim is inaccurate, misleading, or missing important nuance — please open a GitHub Discussion or a pull request. Corrections are welcome, and the goal is an honest comparison rather than marketing copy.