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.

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 (AbstractDomainObject subclasses). Queries use PersistenceManagerInterface::createQueryForType(), and all relation properties are resolved via Extbase's DataMapper.
  • N+1 queries for embedded relations. DataMapper resolves relations through LazyLoadingProxy objects 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 a Record object per row, transforms each field through RecordFieldTransformer, and wraps relations in LazyRecordCollection or RecordPropertyClosure closures. 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 GreedyDatabaseBackend mitigates 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.

  1. No ORM, no object hydration. Records are raw associative arrays from ConnectionPool::getQueryBuilderForTable(). Zero overhead from property mapping, proxy objects, or lazy wrappers.
  2. 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 (...)
  3. Fixed query count. The number of queries is 1 + R (one collection query + one per relation type), not 1 + N×R. Adding more rows to a page does not increase the query count.
  4. 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
Footnotes

(¹) 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.