Resource Definition 

Resources are defined as PHP files placed in any active extension's Configuration/TcaApi/ directory. No manual registration is needed — the extension auto-discovers all *.php files from every active package's Configuration/TcaApi/ directory at boot time and caches the result.

Each file returns a PHP array with the resource configuration.

To modify a resource defined by a third-party package, place an override file in Configuration/TcaApi/Overrides/ — see Overriding Third-Party Resource Configs.

General section 

The general key defines the basic resource properties:

return [
    'general' => [
        'table'        => 'tx_myext_domain_model_article',
        'resourceName' => 'articles',
        'resourceType' => 'Article',
        // operations defaults to ['list', 'show'] — omit for read-only resources
        'itemsPerPage' => 20,
        'storagePid'   => 1,
    ],
];
Copied!
Key Description
table TYPO3 database table name.
resourceName URL slug used in /_api/{resourceName}.
resourceType JSON-LD @type value.
type Set to 'userinfo' to create a userinfo endpoint.
operations Array of enabled operations: list, show, create, update, delete. Defaults to ['list', 'show'] when omitted.
itemsPerPage Default page size for list operations (overrides the global site setting).
maxItemsPerPage Upper limit for itemsPerPage; when set, the requested page size is clamped to this value. No limit when omitted.
storagePid Page ID for newly created records, or a {$site.setting} placeholder resolved per site (see Storage pages per site). Also constrains reads to this page unless readStoragePids is set. When omitted, reads are not pid-constrained and writes target page 0. Any other value fails at boot.
readStoragePids Widens the read-side pid constraint independently of storagePid. Accepts an array ([42, 17, 5]) or comma-separated list ('42,17,5') — reads then use pid IN (...) — or the sentinel '*' to read from all pages regardless of the write target. Entries may be {$site.setting} placeholders. When omitted, reads fall back to storagePid. This is the complete read set: list the write page explicitly if it should also be readable.
recordTypes Scopes the resource to record types of the table's TCA ctrl.type field (CType, doktype, …). Accepts an array (['textmedia', 'header']), a comma-separated list or '*' for every type in $TCA[table]['types']. In default mode each record then exposes only its type's columns; the options form (['include' => …, 'schema' => 'table']) keeps all table columns, and 'split' => true adds one resource per type (see One resource per record type). When omitted, the resource serves records of every type with all columns — see Record types.
writeMode Write execution strategy. acting_user (default) — DataHandler runs under the authenticated user identity, preserving the actor for audit purposes. system_admin — uses a synthetic backend-admin context, bypassing all TYPO3 ACL boundaries. Only enable for fully trusted, internal-only APIs.
group OpenAPI tag grouping this resource's operations in Swagger UI. A string ('Editorial') or an array with a name and optional description. Defaults to resourceType — see Endpoint grouping (OpenAPI tags).

Write mode 

By default, write operations (create, update, delete) run under the acting-user context — TYPO3's DataHandler preserves the caller's identity for audit purposes. Set writeMode to system_admin to bypass TYPO3 ACL checks entirely. Only use this for trusted, back-channel APIs.

'general' => [
    'table'        => 'tx_myext_domain_model_article',
    'resourceName' => 'articles',
    'resourceType' => 'Article',
    'operations'   => ['create', 'update', 'delete'],
    'writeMode'    => 'system_admin',   // default: 'acting_user'
],
Copied!

Storage pages (read vs. write) 

storagePid is the single write target — newly created records land there. By default it also constrains reads to that one page. To read from several pages while still writing into one, add readStoragePids:

'general' => [
    'table'           => 'tx_myext_domain_model_article',
    'resourceName'    => 'articles',
    'resourceType'    => 'Article',
    'operations'      => ['list', 'show', 'create'],
    'storagePid'      => 42,            // writes go here
    'readStoragePids' => [42, 17, 5],   // reads span these pages
],
Copied!

Use the sentinel '*' to read from every page regardless of the write target:

'storagePid'      => 42,    // writes go here
'readStoragePids' => '*',   // reads ignore pid entirely
Copied!

readStoragePids is the complete read set — it does not implicitly include storagePid. List the write page explicitly if it should be readable too.

Storage pages per site 

When one resource serves several sites that keep their records on their own page, point storagePid and readStoragePids at a site setting. The {$…} placeholder is resolved from the settings of the site that receives the request:

'general' => [
    'table'           => 'tx_myext_entry',
    'resourceName'    => 'entries',
    'resourceType'    => 'Entry',
    'storagePid'      => '{$myext.storagePid}',
    'readStoragePids' => ['{$myext.storagePid}', '{$myext.sharedPid}'],
],
Copied!
config/sites/tenant-a/config.yaml
settings:
  myext:
    storagePid: 12
    sharedPid: 3
Copied!

The resolved pid scopes every operation: list and show read only the site's pages, update and delete of another site's record answer 404, and create stores the record, its inline-created relations and its file references on the site's page.

A placeholder fails closed. When the setting is missing, empty, boolean, negative or not an integer, the request answers 500 (the message names the setting when tca_api.debugMode is on) and no query runs without the pid constraint.

Response caching includes the site and its resolved read pages. Changing a storage-page setting cannot reuse a response from the previous scope, and an invalid setting fails even when a response was previously cached.

Record types 

Tables with a TCA ctrl.type field — tt_content.CType, pages.doktype — hold records of several types. recordTypes limits a resource to some of them:

'general' => [
    'table'        => 'tt_content',
    'resourceName' => 'content',
    'resourceType' => 'Content',
    'operations'   => ['list', 'show', 'create', 'update'],
    'recordTypes'  => ['textmedia', 'header'],
],
Copied!

The scope applies to every operation, not only to list: a record of another type answers 404 on show, update and delete, exactly like a record on another page. A filter on the type column cannot do that, because filters only narrow collections.

On writes:

  • A resource with one record type sets the type field on create itself. A client may omit it; a different value is a 422.
  • A resource with several record types requires the type field on create and rejects values outside the list with 422 (INVALID_RECORD_TYPE).
  • update cannot move a record to a type outside the list.

The OpenAPI schema describes the type field as an enum of the allowed types. Unknown types, tables without ctrl.type and foreign-field types ('type' => 'local_field:foreign_field') fail at boot.

Recipe: pages 

pages.doktype works like CType. Most page types are not content for an API: sysfolders (254), the recycler (255), spacers (199) and backend user sections (6). Scope a pages resource to the types a frontend renders:

'general' => [
    'table'        => 'pages',
    'resourceName' => 'pages',
    'resourceType' => 'Page',
    'recordTypes'  => [1, 3, 4],   // standard, link, shortcut
],
Copied!

Pages of other types answer 404, also on show by uid. Each page exposes the fields of its type: a link page carries its URL (link, or url before TYPO3 v14) resolved like any typolink field, and a shortcut page carries shortcut_mode and shortcut as a link to the target page.

Columns per record type 

In default mode (no column declares groups), each record exposes only the columns of its type's form — the fields in the type's showitem, with palettes unpacked, in the same order. A header content element has no bodytext or assets, a textmedia element does. System fields that palettes carry (hidden, starttime, sys_language_uid, …) stay hidden as everywhere else. The type field is always included, even for a type whose form does not show it.

The type's columnsOverrides apply as well. A required or max set for one type is validated only for records of that type, and FlexForm and field processing use the type's configuration.

  • A collection over several types returns each item with its own columns.
  • On writes, columns outside the target type's form are ignored — the type being created or switched to, otherwise the record's current type.
  • The OpenAPI Read schema of a multi-type resource is a oneOf over one schema per type (ContentTextmediaRead, ContentHeaderRead, …) with a discriminator on the type field. A single-type resource's Read and Write schemas describe that type. The Write schema of a multi-type resource lists the columns of all its types, because PATCH bodies are partial and cannot be matched against a single type.

Declared columns keep their configuration but count only for the types whose form contains them. Their TCA-derived validation follows the type's columnsOverrides too, while an explicit required or validator of the same kind still takes precedence. Explicit mode is unaffected: declared columns are the columns.

To scope a resource without changing its columns, use the options form with 'schema' => 'table':

'recordTypes' => [
    'include' => ['textmedia', 'header'],   // default: '*'
    'schema'  => 'table',                   // default: 'type'
],
Copied!

One resource per record type 

'split' => true expands the resource into one resource per included type, for clients that work with one type at a time. Each generated resource is the base config scoped to a single type, so it behaves like a hand-written single-type resource: create sets the type, other types answer 404.

'general' => [
    'table'        => 'tt_content',
    'resourceName' => 'content',
    'resourceType' => 'Content',
    'recordTypes'  => [
        'include'     => ['textmedia', 'header'],   // default: '*'
        'split'       => true,
        'namePattern' => '{resourceName}-{type}',   // default
        'keepBase'    => true,                      // default
    ],
],
'types' => [
    'textmedia' => [
        'security' => ['create' => AccessRole::FE_USER],
    ],
],
Copied!

This registers content-textmedia (ContentTextmedia) and content-header (ContentHeader) next to content.

  • namePattern sets the generated resourceName; the resourceType is its PascalCase form. A name must contain a letter and may contain letters, digits, - and _ only, because it is the URL segment.
  • keepBase => false drops the multi-type base resource. Relations to the table that set no resourceName then link to the first generated resource, which serves only its own type. Keep the base, or set the relation column's resourceName, when other resources link to the table.
  • types holds per-type overrides, merged over the base config of that type's resource only. Nested arrays merge key by key, lists such as operations or the groups of an access role are replaced as a whole. An override may set general.resourceName and general.resourceType as well, but not general.recordTypes.include: a generated resource serves only its own type. types without split fails at boot.
  • The expansion runs after the Overrides/ pass, so an override file that edits the base config reaches every generated resource.
  • Without a group, each generated resource gets its own Swagger UI section, described with the type's label from the TCA items.
  • A generated name that is already taken fails at boot.

Endpoint grouping (OpenAPI tags) 

By default each resource gets its own Swagger UI section, named after its resourceType. Set group to place several resources under a shared, named section:

'general' => [
    'table'        => 'tx_myext_domain_model_article',
    'resourceName' => 'articles',
    'resourceType' => 'Article',
    'group'        => 'Editorial',
],
Copied!

Use the array form to add a description shown above the section:

'group' => [
    'name'        => 'Editorial',
    'description' => 'Editorial content endpoints',
],
Copied!

Point several resources at the same name to merge them into one section. Sections appear in resource-registration order; the first configured description for a group wins. Omitting group falls back to resourceType, so grouping is fully opt-in and backward compatible.

Minimal example (zero-config) 

Three keys are all that is needed for a public read-only resource. operations defaults to ['list', 'show'] and both are PUBLIC by default:

return [
    'general' => [
        'table'        => 'tx_myext_domain_model_article',
        'resourceName' => 'articles',
        'resourceType' => 'Article',
    ],
];
Copied!

To enable write operations, add them explicitly and configure security:

use MaikSchneider\TcaApi\Enum\AccessRole;

return [
    'general' => [
        'table'        => 'tx_myext_domain_model_article',
        'resourceName' => 'articles',
        'resourceType' => 'Article',
        'operations'   => ['list', 'show', 'create', 'update', 'delete'],
        'storagePid'   => 1,
    ],
    'security' => [
        'create' => AccessRole::FE_USER,
        'update' => AccessRole::FE_USER,
        'delete' => AccessRole::BE_ADMIN,
    ],
];
Copied!

Full example 

use MaikSchneider\TcaApi\Enum\AccessRole;
use MaikSchneider\TcaApi\Filter\ExactFilter;
use MaikSchneider\TcaApi\Filter\MmFilter;
use MaikSchneider\TcaApi\Serializer\FileProcessing\FileProcessor;
use MaikSchneider\TcaApi\Serializer\Processing\TypoLinkProcessor;

return [
    'general' => [
        'table'        => 'tx_myext_domain_model_article',
        'resourceName' => 'articles',
        'resourceType' => 'Article',
        'operations'   => ['list', 'show', 'create', 'update', 'delete'],
    ],
    'columns' => [
        'title' => [
            'type'       => 'string',
            'groups'     => ['list', 'show', 'create', 'update'],
            'required'   => true,
            'validators' => [
                ['type' => 'maxLength', 'max' => 20],
                ['type' => 'minLength', 'min' => 3],
                ['type' => 'regex', 'pattern' => '/^[\w\s]+$/u'],
            ],
        ],
        'color_id'   => ['groups' => ['list', 'show', 'create', 'update']],
        'categories' => ['groups' => ['list', 'show', 'create', 'update']],
        'profile_photo' => ['groups' => ['list', 'show']],
        'downloads' => [
            'groups'    => ['list', 'show'],
            'processor' => FileProcessor::class,
        ],
        'article_url' => [
            'groups'    => ['list', 'show'],
            'processor' => TypoLinkProcessor::class,
        ],
    ],
    'filters' => [
        'title'      => ExactFilter::class,
        'color_id'   => ExactFilter::class,
        'categories' => MmFilter::class,
    ],
    'order' => [
        'allowed' => ['title', 'uid'],
        'default' => ['uid' => 'asc'],
    ],
    'security' => [
        'list'   => AccessRole::PUBLIC,
        'show'   => AccessRole::PUBLIC,
        'create' => AccessRole::FE_USER,
        'update' => AccessRole::FE_USER,
        'delete' => AccessRole::BE_ADMIN,
    ],
];
Copied!