TYPO3 Camp Berlin 2026
TCA rein,
JSON-LD raus.
Einfach konfiguriert, schnell ausgeliefert:
eine REST-API für jede TCA-Tabelle mit tca_api
Maik Schneider
Technical Lead TYPO3 · xima
github.com/maikschneider/tca-api
TYPO3 Camp Berlin 2026 · Maik Schneider
Einfach konfiguriert, schnell ausgeliefert: eine REST-API für jede TCA-Tabelle mit tca_api. Alle Folien zum Durchblättern, die Messwerte stammen aus dem Benchmark in diesem Repository.
TYPO3 Camp Berlin 2026
Einfach konfiguriert, schnell ausgeliefert:
eine REST-API für jede TCA-Tabelle mit tca_api
Maik Schneider
Technical Lead TYPO3 · xima
github.com/maikschneider/tca-api
GET /_api/articles
1
Welches Vokabular gilt
2
Adresse und Typ jeder Ressource
3
Pagination nach Standard
4
Nur die @id, der Client löst sie auf
5
Ganzer Datensatz, kein zweiter Request
{
"@context": "http://www.w3.org/ns/hydra/context.jsonld", ← 1
"@type": "hydra:Collection",
"hydra:totalItems": 10000, ← 3
"hydra:member": [{
"@id": "/_api/articles/4", ← 2
"@type": "Article",
"title": "Caching in TYPO3",
"reading_time": 12,
"author": "/_api/authors/7", ← 4
"tags": [{"@id": "/_api/tags/1", "@type": "Tag", ← 5
"title": "PHP"}, …]
}, … 19 weitere],
"hydra:view": { ← 3
"@type": "hydra:PartialCollectionView",
"hydra:first": "/_api/articles?page=1",
"hydra:next": "/_api/articles?page=2",
"hydra:last": "/_api/articles?page=500"
}
}
GET /_api/articles?page=1&itemsPerPage=100
aus 10.000, mit Gesamtzahl und Pagination
title, teaser, published, reading_time
1 Autor, 2–4 Tags, 1–3 Kategorien je Artikel
eingebettete Datensätze ohne weitere Relationen
| API | Zeilen Config/Code | Queries | App-Zeit |
|---|---|---|---|
| tca_api | 30 | 5 | 15,7 ms |
| sg_apicore¹ | 27 | 216 | 49,0 ms |
| t3api | 184 | 304 | 412,4 ms |
| Record API, von Hand | 201 | 305 | 154,4 ms |
| nnrestapi | 221 | 307 | 673,4 ms |
| Extbase, von Hand | 281 | 307 | 148,6 ms |
¹ ohne Kategorien: sg_apicore kann sie nicht einbetten, die Response enthält also eine Relation weniger.
Kalter Cache · TYPO3 14.3.8 · Production-Modus · k6, 8 virtuelle User · Median aus 3 Läufen
Warum noch eine API-Extension?
01
Static File Cache ist extrem performant, also kein USER_INT
02
Dynamische Daten-Elemente überall platzieren
03
Liefern die dynamischen Daten, erste Wahl: t3api
04
Häufige Änderungen, im Extremfall tausende Datensätze im Client: Serialisierung wird zur Grenze
05
So einfach wie t3api, ohne Limit bei der Performance
Das Ziel: einfach konfigurieren und schnell ausliefern, beides zugleich.
06 · Beispiel Personenverzeichnis
Backend: neue Person, Austritt
Frontend-Update: Profil gepflegt
Deployment
07:00
10:00
13:00
16:00
19:00
Jeder Strich leert den Cache. Der nächste Request ist kalt.
Der Engpass bei t3api:
die Serialisierung
Die Extension
GitHub
github.com/maikschneider/tca-api
TER
extensions.typo3.org/extension/tca_api
Doku
docs.typo3.org/p/maikschneider/tca-api
Version 2.2.0 · TYPO3 13.4 und 14.3 · PHP 8.2+ · GPL-2.0-or-later
1
composer require \
maikschneider/tca-api
2
dependencies:
- maikschneider/tca-api
3
Configuration/TcaApi/Articles.php
→ GET /_api/articles
Schritt 3 · Ressource anlegen
// Configuration/TcaApi/Articles.php
return [
'general' => [
'table' => 'tx_bench_article',
'resourceName' => 'articles',
'resourceType' => 'Article',
],
];
Man bekommt: Liste, Detail, Pagination, Hydra JSON-LD, OpenAPI und Swagger UI.
Man spart sich: Controller, Extbase-Models, Routen, generierte Klassen.
Felder und Typen kommen aus dem TCA. Nichts läuft auseinander.
Und im Backend?
1
Die fertige Konfiguration aller Ressourcen als durchsuchbarer Baum
2
Swagger UI mit OpenAPI 3.1, zum Ausprobieren oder als openapi.json
Highlights
01
Autor und Tags im selben Request
02
auch über Relationen hinweg
03
alle Crop Variants, fertig verarbeitet
04
ohne TypoScript
05
Schreiben mit Datei in einem Request
Ohne Konfiguration dabei: Mehrsprachigkeit, Pagination, OpenAPI und Swagger UI.
Anwendungsfall 1 · Relationen
Konfiguration · Articles.php
'columns' => [
'author' => ['embed' => true],
'tags' => ['embed' => true],
],
Ergebnis
GET /_api/articles
{
"title": "Caching in TYPO3",
"author": {
"@id": "/_api/authors/7",
"name": "Ada Lovelace"
},
"tags": [{"title": "PHP"}, {"title": "Cache"}]
}
Eine Query pro Relation, egal ob 20 oder 1.000 Datensätze.
Anderswo: Model pro Tabelle, Mapping, Serializer-Gruppen und oft eine Query pro Datensatz.
Anwendungsfall 2 · Filter und Suche
Konfiguration · Articles.php
'filters' => [
'categories.title' => ExactFilter::class,
'q' => [SearchFilter::class, [
'columns' => ['title', 'author.name'],
]],
],
Ergebnis
GET /_api/articles
?filters[categories.title]=Technik
&filters[q]=hopper
→ nur Artikel aus „Technik“,
deren Titel oder Autor „hopper“ enthält
Ein Punkt im Namen genügt, um über Relationen zu filtern.
Anderswo: pro Filter eine eigene Repository-Methode mit Joins.
Anwendungsfall 3 · Bilder
Konfiguration · Articles.php
'columns' => [
'image' => ['image' => [
'maxWidth' => 1200,
'fileExtension' => 'webp',
]],
],
Ergebnis
"image": {
"publicUrl": "/fileadmin/hero.jpg",
"metadata": {"alternative": "…"},
"cropVariants": {
"desktop": {"publicUrl": "…/hero_d.webp"},
"mobile": {"publicUrl": "…/hero_m.webp"}
}
}
Die Crops aus dem Backend kommen fertig verarbeitet an.
Anderswo: pro Variante ein Getter mit eigener Bildverarbeitung.
Anwendungsfall 4 · Links
Konfiguration · News.php
// Link-Felder: nichts zu tun
'virtualProperties' => [
'url' => [
'processor' => RouteEnhancerProcessor::class,
'route' => [
'pid' => 12, 'extension' => 'News', …
'arguments' => ['news' => '{uid}'],
],
],
],
Ergebnis
GET /_api/news/4
{
"title": "Caching in TYPO3",
"link": "https://typo3.org",
"url": "/news/caching-in-typo3"
}
Jedes Link-Feld wird automatisch aufgelöst, Detail-URLs laufen über den Site-Router.
Anderswo: TypoScript-Setup plus eigener Link-Builder pro Datensatz.
Anwendungsfall 5 · Formulare
Konfiguration · Articles.php
'general' => [
…
'operations' => ['list', 'show', 'create'],
'storagePid' => 2,
],
'security' => ['create' => AccessRole::FE_USER],
'columns' => [
'image' => ['upload' => [
'folder' => '1:/user_upload/',
]],
],
Ergebnis
curl -X POST /_api/articles \
-F "title=Neuer Artikel" \
-F "image=@foto.jpg"
→ 201 Created
→ 422 mit Feldfehlern, Regeln aus dem TCA
Gespeichert wird über den DataHandler: Hooks, Historie und Cache-Leerung inklusive.
Anderswo: Controller, Validatoren, Datei-Handling und oft ein zweiter Request für den Upload.
Performance
Die Antwort liegt fertig bereit. Hier ist fast jede API schnell.
Der Normalfall zwischen zwei Änderungen
Der erste Request nach jeder Änderung. Jetzt zählen Queries und Serialisierung.
Im Personenverzeichnis: ständig
So wurde gemessen
6 APIs, dieselbe Aufgabe, jede im eigenen Paket mit eigenem Cache · TYPO3 14.3.8, Production-Modus · k6 mit 8 virtuellen Usern · Median aus 3 Läufen · Code und Rohdaten öffentlich
Warmer Cache · 100 Datensätze
Record API, von Hand
14 ms
t3api
21 ms
Extbase, von Hand
21 ms
tca_api
35 ms
sg_apicore¹
36 ms
nnrestapi
90 ms
Warm entscheidet der Cache, nicht die API. Die Record API mit eigenem Antwort-Cache ist hier vorn.
Antwortzeit p50 · TYPO3 14.3.8, Production-Modus · k6, 8 virtuelle User · Median aus 3 Läufen · ¹ ohne Kategorien
Kalter Cache · 100 Datensätze
tca_api
57 ms
sg_apicore¹
107 ms
Record API, von Hand
216 ms
Extbase, von Hand
248 ms
t3api
481 ms
nnrestapi
716 ms
Ohne Cache zählt, wie viele Queries eine API absetzt: tca_api 5, die anderen 216 bis 307.
Antwortzeit p50 · TYPO3 14.3.8, Production-Modus · k6, 8 virtuelle User · Median aus 3 Läufen · ¹ ohne Kategorien, eine Relation weniger
Und warum ist das schnell?
01
Ein PHP-Array pro Ressource. Keine Controller, Models oder generierten Klassen.
my_ext/
└── Configuration/
└── TcaApi/
├── Articles.php ← Ressource
├── People.php
└── Overrides/
└── Extend.php ← erweitern
Erweitern wie im TCA: Overrides/
02
Von der Datenbankzeile direkt zu JSON-LD.
TCA
Spalten, Typen, Relationen
↓
QueryBuilder-Zeilen
einfache Arrays
↓
Nach TCA-Typ normalisiert
Datum, Bilder, Links, Relationen
↓
Hydra JSON-LD
kompatibel mit API Platform
Kein DataMapper, keine Hydration, keine Proxies
03
Eine Query pro Relation, nie eine pro Zeile.
Cache-Lookup
Treffer: 0 Queries
↓
Basis-Query
Filter, Sortierung, Seite, Count
↓
EmbedPreloader
ein WHERE uid IN (…) pro Relation
↓
JSON-LD
mit Cache-Tags gespeichert
Invalidierung über den DataHandler
━ tca_api ━ Extbase, von Hand (Record API, t3api, nnrestapi verlaufen gleich) · Queries pro Request, kalter Cache, log. Skala
10k
1.000
100
10
1
1
5
20
50
100
500
1.000 Datensätze
5
3.007
Kalter Cache · 1.000 Datensätze
tca_api
140 ms
Record API, von Hand
1.524 ms
Extbase, von Hand
1.782 ms
nnrestapi
4.685 ms
t3api
6.045 ms
Genau der Fall aus dem Personenverzeichnis: alle Daten auf einmal zum Client.
Antwortzeit p50 · TYPO3 14.3.8, Production-Modus · k6, 8 virtuelle User · Median aus 3 Läufen · sg_apicore fehlt: höchstens 100 Datensätze pro Seite
sg_apicore
27 Zeilen · 0 Klassen
tca_api
30 Zeilen · 0 Klassen
t3api
184 Zeilen · 4 Klassen
Record API
201 Zeilen · 3 Klassen
nnrestapi
221 Zeilen · 7 Klassen
Extbase
281 Zeilen · 6 Klassen
Gezählt pro Benchmark-Paket: PHP-, YAML- und Config-Dateien für dieselbe Artikel-Aufgabe, ohne Site-Set-Dependency oder Routen-Import.
TCA rein, JSON-LD raus.
Extension
github.com/maikschneider/tca-api
Benchmark
github.com/maikschneider/typo3-api-benchmark
Kontakt
Maik Schneider
← → · F · Esc