Table of Contents

  1. Ext: sg_apicore
  2. sg_apicore for TYPO3
  3. APIs & Registration
  4. Writing Endpoints
  5. TCA Mapper
  6. Auto-CRUD Resources
  7. OpenAPI Documentation
  8. Authentication & Scopes
  9. Logging
  10. Tenants
  11. Migration Guide: sg_rest to sg_apicore

Ext: sg_apicore

License: GNU GPL, Version 2

Repository: https://gitlab.sgalinski.de/typo3/sg_apicore

Please report bugs here: https://gitlab.sgalinski.de/typo3/sg_apicore/-/issues

Short Summary

Provides an API framework for TYPO3: Multi-API, Multi-Tenants, Attribute-based endpoint configuration, Logging, Token JWT Bearer auth, User auth, Entity CRUD registration, Custom Endpoints.

For detailed information, please refer to the Documentation in docs/. For website-ready end-user communication, see End-User-Documentation.

Directory Structure

The extension follows a standard TYPO3 extension structure with a focus on clean separation of concerns:

  • Classes/
    • Attribute/: PHP attributes for routing, configuration, and security (e.g., #[ApiRoute], #[RequireScopes]).
    • Configuration/: Configuration readers and objects.
    • Context/: Value objects for request context (e.g., TenantContext).
    • Controller/: API controllers handling the requests.
    • Domain/:
      • Repository/: Repositories for database access (e.g., TokenRepository).
    • Middleware/: PSR-15 middlewares (e.g., ApiRequestMiddleware for request interception).
    • Security/: Authentication and authorization logic (e.g., BearerTokenProvider, AuthContext).
    • Service/
      • Tenant/: Tenant resolution logic and resolvers.
      • ApiRegistry.php: Service to register APIs and versions.
      • Router.php: FastRoute-based dispatcher.
  • Configuration/: TYPO3 configuration files (Services, Middlewares, TCA).
  • docs/: Technical documentation and guides.
  • tests/: Unit and functional tests.

Installation

  1. Install the extension via composer:

    composer require sgalinski/sg-apicore
    
  2. Activate the extension in the TYPO3 Extension Manager.

Quick Start (3 Steps)

1. Register your Controller

Add your controller to Configuration/Services.php and tag it with sg_apicore.router:

$services->set(MyController::class)
    ->tag('sg_apicore.router');

2. Define an Endpoint

Use the #[ApiRoute] attribute in your controller action:

#[ApiRoute(path: '/hello', methods: ['GET'])]
public function helloAction(ServerRequestInterface $request): ResponseInterface {
    return $this->responseService->createSuccessResponse(['message' => 'Hello!']);
}

3. Access the API

Open your browser at https://your-domain.local/api/docs/ui/ to see the generated Swagger UI and test your new endpoint!

Testing

You can test the API by calling the health endpoint:

# Basic health check
curl https://your-project.local/api/health

# API-specific health check (if registered)
curl https://your-project.local/api/public/v1/health

The API path prefix is configurable via the extension configuration (default: /api/).

API Registration

To register a new API, you use the ApiRegistry service. Detailed configuration options can be found in the APIs & Registration Documentation.

use SGalinski\SgApiCore\Service\ApiRegistry;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$apiRegistry = GeneralUtility::makeInstance(ApiRegistry::class);
$apiRegistry->registerApi('public', ['1']);

Writing Endpoints

Endpoints are defined using PHP attributes on controller methods. See Writing Endpoints for a full guide. For a complete template with current best practices, see ExampleController.

#[ApiRoute(path: '/my-endpoint', methods: ['GET'], apiId: 'public', version: '1')]
public function myAction(ServerRequestInterface $request): ResponseInterface {
    return $this->responseService->createSuccessResponse(['message' => 'Hello World']);
}

Standardized Responses

The ResponseService provides a unified way to create JSON responses, following RFC 7807 for errors. See Writing Endpoints - Responses.

Pagination

The extension provides a PaginationService to handle consistent pagination across endpoints. See Writing Endpoints - Pagination (Note: Add pagination details to docs if missing).

TCA Mapper

The TcaMapper service allows you to map TYPO3 database records to API response arrays based on the TCA. See TCA Mapper Documentation.

Auto-CRUD Resources

You can expose TYPO3 tables as API resources with full CRUD support. See Auto-CRUD Resources Documentation.

OpenAPI Documentation

The extension automatically generates OpenAPI 3.0 specifications. You can access Swagger UI at /api/{apiId}/v{version}/docs/ui. See OpenAPI Documentation.

Backend Module

The extension provides a TYPO3 Backend Module under System > API Core.

  • APIs & Versions: Overview and Swagger UI links.
  • Token Management: Create and manage Opaque and Refresh tokens.
    • Supports optional FE-user bound tokens (user_id mapped to fe_users) for per-user API key flows.
    • Token list keeps current filter state while editing/revoking/regenerating.
  • Endpoints: List of all registered endpoints and their requirements.

See Authentication & Scopes - Backend for details.

Logging

Comprehensive logging for API requests and responses, including request tracking via a unique Request ID. See Logging Documentation.

Multi-Tenancy

Every API request runs in a TenantContext, usually derived from the TYPO3 Site. Endpoints can be filtered by tenants using the tenants property in the #[ApiRoute] attribute. See Tenants Documentation.

Security & Authentication

Supports multiple auth modes (public, token, user) and scope-based authorization.

  • API Level: Define the default authMode as a string in the ApiRegistry.
  • Endpoint Level: Override or extend the authMode using the #[ApiRoute] attribute (supports string or array, e.g., ['public', 'user']).

See Authentication & Scopes.

CORS

CORS handling is provided by ApiCorsMiddleware and is enabled for API paths (apiPathPrefix) by default.

  • Origin policy is default deny.
  • Preflight requests (OPTIONS with Access-Control-Request-Method) are answered directly.
  • Allowed origins are configured per API via ApiRegistry::registerApi(..., $security):
$apiRegistry->registerApi('partner', ['1'], [
    'authMode' => 'user',
    'authProviders' => ['beareropaquetokenprovider'],
    'cors' => [
        'allowedOrigins' => ['https://app.example.org'],
        'allowCredentials' => true,
    ],
]);

Known Issues & Troubleshooting

Missing Authorization Header (Apache)

In some hosting environments (especially Apache with PHP via CGI/FastCGI), the Authorization header is stripped before it reaches PHP. If you experience "Authentication required" errors despite sending a valid token, add the following to your .htaccess file:

SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1

The extension includes a fallback to check for HTTP_AUTHORIZATION and REDIRECT_HTTP_AUTHORIZATION, but this server-side configuration is the most reliable fix.

Legacy Support (Migration from sg_rest)

The extension provides a bridge to support legacy sg_rest clients. This includes:

  • Middleware for mapping old URL patterns.
  • Support for fe_users authentication tokens.
  • Emulation of the old response format.

Note: Legacy support is disabled by default. See the Migration Guide for details on how to enable and use it.

Architectural Decisions

For information on why we chose certain technologies and patterns, see our Architectural Decision Records at docs/adr/.


sg_apicore for TYPO3

sg_apicore is a high-performance API framework for TYPO3 projects.
It enables teams to build secure, documented, and maintainable APIs directly in TYPO3 without adding a separate API platform.

This text is designed for website publication and product communication.

What sg_apicore Solves

Modern TYPO3 projects often need more than page rendering:

  • headless frontends and apps
  • partner portals and data integrations
  • mobile apps and authenticated user APIs
  • high-throughput machine-to-machine APIs

sg_apicore provides one consistent framework for these use cases, including routing, authentication, scopes, OpenAPI docs, caching, and observability.

Core Benefits

  • Fast API routing based on PHP attributes
  • Multiple APIs and versions in parallel
  • Secure token and user authentication modes
  • Fine-grained access control with scopes
  • Automatic OpenAPI and Swagger UI generation
  • Built-in response caching and cache invalidation
  • Optional multi-tenant operation (site-based by default)
  • TYPO3 backend module for API and token operations
  • Legacy migration bridge for old sg_rest setups

Typical Use Cases

  • Public read-only content APIs
  • Partner APIs with scope-based access
  • User APIs with login, access token, and refresh token
  • BFF endpoints for SPAs and native apps
  • Controlled CRUD endpoints for TYPO3 tables

Authentication and Security

sg_apicore supports multiple authentication models:

  • public: no token required
  • token: machine token (opaque bearer)
  • user: user login with access and refresh token
  • backend: backend session based endpoints (internal use cases)

Security features include:

  • Scope enforcement per endpoint
  • Optional RequireUser checks for true user context
  • RFC 7807 compliant error responses
  • Request IDs for tracing and support
  • Configurable key redaction in logs
  • Configurable rate limiting

API Documentation for Consumers

Every registered API can expose:

  • JSON specification: /api/{apiId}/v{version}/docs.json
  • Swagger UI: /api/{apiId}/v{version}/docs/ui

This lets internal and external consumers test endpoints, inspect schemas, and integrate faster.

Operations and Performance

sg_apicore is built for production workloads:

  • Response caching for GET requests
  • Cache control and tag-based invalidation
  • Optional language and user-group aware cache variation
  • Rate-limit headers for client-side handling
  • Dedicated backend dashboard for API visibility

Backend Module Highlights

In TYPO3 backend under System > API Core, teams can:

  • inspect registered APIs and endpoints
  • create and revoke API tokens
  • manage token scopes and expiry
  • inspect endpoint requirements
  • monitor rate limits and logs

Multi-Tenant Readiness

Each request is processed in a tenant context.
By default, tenant resolution is site-aware and can be extended through resolver chains.

This allows one TYPO3 instance to provide separated API behavior for multiple sites or clients.

Why Teams Choose sg_apicore

  • Uses TYPO3-native concepts instead of a disconnected external stack
  • Clear and auditable endpoint definitions via attributes
  • Good developer experience with generated API docs
  • Reduced integration risks through standardized responses and validation
  • Proven in active production projects with sustained load

Recommended Rollout Steps

  1. Define your API IDs and versions.
  2. Select auth modes per API (public, token, user, backend).
  3. Configure scopes and rate limits.
  4. Enable OpenAPI endpoints for internal and partner onboarding.
  5. Add caching rules for high-traffic read endpoints.
  6. Use backend token management for secure key lifecycle.

Further Technical Documentation

For implementation details, see:

  • docs/APIs.md
  • docs/WritingEndpoints.md
  • docs/AuthScopes.md
  • docs/OpenAPI.md
  • docs/RateLimiting.md
  • docs/Tenants.md

APIs & Registration

sg_apicore supports multiple APIs in parallel, each of which can have its own versions and security configurations.

API Registration

Registration takes place in your extension's ext_localconf.php via the ApiRegistry service.

use SGalinski\SgApiCore\Service\ApiRegistry;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$apiRegistry = GeneralUtility::makeInstance(ApiRegistry::class);

// Registers a public API
$apiRegistry->registerApi('public', ['1'], [
    'authMode' => 'public'
]);

// Registers a partner API with token authentication and rate limits
$apiRegistry->registerApi(
	'partner',
	['1', '2'],
	[
		'authMode' => 'token'
	],
	NULL,
	[
		'rateLimit' => [
			'limit' => 120,
			'windowSeconds' => 60,
			'burst' => 30
		]
	]
);

Configuration Options

When registering, the following options can be passed in the third parameter:

  • authMode (string): Defines the default authentication mode for all endpoints of this API.
    • public: No token required (unless explicitly required via attribute).
    • token: A valid Opaque Bearer Token is required.
    • user: A user login (Access/Refresh Token) is required.
    • backend: A valid TYPO3 backend user session is required.
  • authProviders: List of allowed providers (e.g., ['beareropaquetokenprovider', 'backenduserprovider']).

Backend API Example

For internal APIs that should only be accessible to logged-in TYPO3 backend users, you can use the backend authMode:

$apiRegistry->registerApi('backend', ['1'], [
	'authMode' => 'backend',
	'authProviders' => ['backenduserprovider'],
]);

This configuration ensures that the API is only accessible if a valid backend user session exists. The backenduserprovider automatically resolves the user and provides standard scopes like backend, partner:read, partner:write, and user.

Use the fourth parameter to override the base path (default: /api/{apiId}/v{version}).

Endpoint Overrides

While the API-level authMode must be a string, individual endpoints can define multiple modes using an array in the #[ApiRoute] attribute:

// Available via public access OR with a valid user token
#[ApiRoute(path: '/login', methods: ['POST'], authMode: ['public', 'user'])]

Use the fifth parameter for additional options:

  • rateLimit: Overrides rate limit settings for this API (see RateLimiting.md).

Versioning

The version is specified in the URL with the prefix v, e.g., /api/public/v1/.... The ApiRegistry checks whether the requested version is enabled for the respective API ID.


Writing Endpoints

In sg_apicore, endpoints are defined via standard PHP classes (controllers) configured using PHP attributes.

Recommended Template

Use docs/examples/ExampleController.php as the reference implementation. It covers:

  • global and API-specific routes
  • tenant and auth context usage
  • query/path/body validation
  • RequireUser and RequireScopes
  • pagination with metadata
  • response caching via ApiCache
  • full TypoScript requirement via RequireFullTypoScript

Controller Registration

For the router to recognize your controller class, it must be registered in Configuration/Services.php with the sg_apicore.router tag:

$services->set(MyCustomController::class)
	->tag('sg_apicore.router');

Routing & Metadata

Use the #[ApiRoute] attribute to define the path and methods. Additional attributes serve the automatic OpenAPI documentation.

use SGalinski\SgApiCore\Attribute\ApiRoute;
use SGalinski\SgApiCore\Attribute\ApiEndpoint;
use SGalinski\SgApiCore\Attribute\ApiResponse;
use SGalinski\SgApiCore\Service\ResponseService;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

class MyController {
	public function __construct(
		protected ResponseService $responseService
	) {}

	#[ApiRoute(path: '/my-action/{id}', methods: ['GET'], apiId: 'public', version: '1', tenants: 'my-tenant')]
	#[ApiEndpoint(summary: 'Short description', tags: ['MyCategory'])]
	#[ApiResponse(status: 200, description: 'Success')]
	public function myAction(ServerRequestInterface $request, string $id): ResponseInterface {
		$data = ['id' => $id, 'name' => 'Test'];
		return $this->responseService->createSuccessResponse($data);
	}
}

Endpoint Filtering

By default, an endpoint is available for all registered APIs and versions. You can restrict an endpoint to specific APIs, versions, tenants, or auth modes by using the properties of the ApiRoute attribute:

// Only available for /api/public/v1/...
#[ApiRoute(path: '/public-only', methods: ['GET'], apiId: 'public', version: '1')]

// Only available for specific tenants (Site-ID by default)
#[ApiRoute(path: '/tenant-specific', methods: ['GET'], tenants: 'citypower-tenant')]
#[ApiRoute(path: '/multi-tenant', methods: ['GET'], tenants: ['tenant-a', 'tenant-b'])]

// Available for both public and partner APIs in version 1
#[ApiRoute(path: '/v1-shared', methods: ['GET'], apiId: ['public', 'partner'], version: '1')]

// Publicly accessible even in a protected 'user' or 'token' API
#[ApiRoute(path: '/auth/login', methods: ['POST'], authMode: 'public')]

Manual Property Mapping & Validation (Extbase Compatibility)

Since this extension avoids the Extbase bootstrap for performance reasons, automatic argument mapping is not available. You can still use Extbase's tools manually:

use TYPO3\CMS\Extbase\Property\PropertyMapper;
use TYPO3\CMS\Extbase\Validation\ValidatorResolver;

// Inside your controller action:
$myModel = $this->propertyMapper->convert($data, MyModel::class);
$results = $this->validatorResolver->getBaseValidatorConjunction(MyModel::class)->validate($myModel);

Request Context

During request handling, sg_apicore enriches the PSR-7 request with attributes. Most relevant ones:

  • api.tenant (TenantContext)
  • api.auth (AuthContext, if authenticated)
  • api.requestId (string)
  • frontend.typoscript (when #[RequireFullTypoScript] is active)

Parameter Types & Validation

The extension supports automatic validation of parameters based on the attributes used in your controller.

  • Path Parameters: Defined via #[ApiPathParam]. Passed directly as arguments to the method (e.g., $id).
  • Query Parameters: Defined via #[ApiQueryParam]. Access via $request->getQueryParams().
  • Body Parameters: Defined via #[ApiBodyParam]. Access via $request->getParsedBody() (JSON bodies are automatically parsed by the middleware).

Validation Constraints

You can add various validation constraints to these attributes:

  • type: The expected data type (string, integer, float, boolean, array, file).
  • required: Whether the parameter must be present.
  • pattern: An optional regular expression (PCRE) the value must match.
  • min / max: For numeric types, defines the inclusive range.
  • minLength / maxLength: For string types, defines the length range.
  • requiredIf: Makes a field required only if a certain condition is met.
    • Example: requiredIf: 'type=special' (field is required if the field type has the value special).
    • Example: requiredIf: 'otherField' (field is required if the field otherField is present and not empty).

Requirement of TypoScript

Some endpoints might require the full TYPO3 TypoScript configuration for parsing (e.g., using lib.parseFunc_RTE) or rendering content. By default, the API context only provides a minimal TypoScript stub for performance reasons.

You can signal that an endpoint requires the full TypoScript to be loaded by using the #[RequireFullTypoScript] attribute:

#[ApiRoute(path: '/render-content', methods: ['GET'])]
#[RequireFullTypoScript]
public function renderAction(ServerRequestInterface $request): ResponseInterface {
	// The full TypoScript setup is now available via $request->getAttribute('frontend.typoscript').
}

Note: Loading the full TypoScript can significantly impact the performance of the API request as it triggers the TYPO3 TypoScript parsing and potentially caching mechanisms. Only use it if absolutely necessary.

Example:

#[ApiRoute(path: '/register', methods: ['POST'])]
#[ApiBodyParam(name: 'username', type: 'string', minLength: 5, maxLength: 20, pattern: '/^[a-z0-9_]+$/')]
#[ApiBodyParam(name: 'age', type: 'integer', min: 18)]
#[ApiBodyParam(name: 'type', type: 'string')]
#[ApiBodyParam(name: 'company_name', required: false, requiredIf: 'type=business')]
public function registerAction(ServerRequestInterface $request): ResponseInterface {
	// ...
}

Automatic Validation from TCA

For Auto-CRUD resources, validation rules are automatically derived from the TYPO3 TCA:

  • eval => required results in required: true.
  • eval => email results in a standard email regex pattern.
  • type => number results in integer or float type validation.
  • config => range results in min and max validation.

Standardized Responses

Use the ResponseService for consistent JSON responses:

  • createSuccessResponse($data, $meta, $status): Generates a success response (optional with envelope).
  • createErrorResponse($title, $detail, $status): Generates an RFC 7807 compliant error message.

Pagination

The extension provides a PaginationService to handle consistent pagination across endpoints.

Using the PaginationService

use SGalinski\SgApiCore\Service\PaginationService;

class MyController {
	public function __construct(
		protected PaginationService $paginationService,
		protected ResponseService $responseService
	) {}

	#[ApiQueryParam(name: 'page', type: 'integer', description: 'The page number (1-based)')]
	#[ApiQueryParam(name: 'limit', type: 'integer', description: 'Maximum number of items to return')]
	public function listAction(ServerRequestInterface $request): ResponseInterface {
		$pagination = $this->paginationService->getPaginationParams($request);
		$offset = $pagination['offset'];
		$limit = $pagination['limit'];

		$items = $this->myRepository->findSubset($limit, $offset);
		$total = $this->myRepository->countAll();

		return $this->responseService->createSuccessResponse(
			$items,
			$this->paginationService->buildPaginationMeta($total, $offset, $limit)
		);
	}
}

The pagination metadata will be included in the meta object of the response (if the envelope is enabled or if meta is explicitly passed).

Performance & Caching

The extension includes a built-in response caching system based on the TYPO3 Caching Framework.

Default Behavior

Caching is enabled by default for all GET requests. The cache key automatically varies by:

  • The full Request URI
  • The current Site and Language
  • The Frontend User Groups (sorted to ensure consistency)

Controlling Cache

You can customize or disable caching using the #[ApiCache] attribute:

use SGalinski\SgApiCore\Attribute\ApiCache;

// Disable caching for this endpoint
#[ApiRoute(path: '/live-data', methods: ['GET'])]
#[ApiCache(enabled: false)]
public function liveAction(): ResponseInterface { ... }

// Customize caching
#[ApiRoute(path: '/heavy-list', methods: ['GET'])]
#[ApiCache(lifetime: 3600, tags: ['news', 'category_1'])]
public function listAction(ServerRequestInterface $request): ResponseInterface {
	// This response will be cached for 1 hour with specific tags.
}

Cache Configuration

The #[ApiCache] attribute supports the following properties:

  • enabled (bool): Whether caching is enabled. Default is true.
  • lifetime (int): Cache lifetime in seconds. Default is 0 (system default).
  • tags (array): A list of cache tags. Highly recommended for selective invalidation.
  • useUserGroups (bool): If true (default), the cache key varies by the user groups of the authenticated frontend user.
  • useLanguage (bool): If true (default), the cache varies by the current language.
  • additionalVary (array): Additional query parameters or header names that should affect cache keys.

Cache Invalidation

The system automatically performs tag-based invalidation if a writing request (POST, PATCH, DELETE) is made to an endpoint that defines the same cache tags.

For example, if a POST request is sent to an endpoint with #[ApiCache(tags: ['news'])], all cache entries with the news tag will be flushed.

Note: For automatic resources (CRUD), caching and invalidation are handled automatically using the table name as a cache tag.

Clearing Cache manually

You can clear the entire API response cache in the TYPO3 Backend via the "Flush cache" menu (lightning icon) using the "Clear API Cache" entry. This entry is only available to backend administrators.

Cache Status Headers

You can monitor the cache status via the X-TYPO3-API-Cache HTTP header in the response:

  • HIT: The response was served directly from the cache.
  • MISS: The response was newly generated and then stored in the cache.

TCA Mapper

The TcaMapper service allows TYPO3 database records (arrays) to be automatically converted into API-compliant structures. It uses the information from the Table Configuration Array (TCA).

Features

  • Automatic Whitelisting: Internal TYPO3 fields (such as tstamp, crdate, hidden) are excluded by default.
  • Type Conversion:
    • Booleans (for type => check)
    • Integers (for eval => int or type => number)
    • Date formats (ISO 8601 strings for inputDateTime)
  • Multi-Value Support: Comma-separated lists (e.g., relations) are automatically converted into arrays.
  • Relation Resolution: Automatically resolves FAL (sys_file_reference), 1:n (IRRE) and M:M (MM table) relations.
  • Custom Callbacks: Support for computed or dynamic fields during mapping.
  • Field Renaming: Ability to expose database fields under different names in the API.
  • Field Configurations: Granular control over allowed/excluded fields per table, including nested relations.

Usage in Controller

use SGalinski\SgApiCore\Mapper\TcaMapper;

class MyController {
    public function __construct(
        protected TcaMapper $tcaMapper,
        protected ResponseService $responseService
    ) {}

    public function getAction(ServerRequestInterface $request, string $id): ResponseInterface {
        $record = $this->myRepository->findByUid($id);

        // Mapping for tt_content
        $mappedData = $this->tcaMapper->mapRecord('tt_content', $record);

        return $this->responseService->createSuccessResponse($mappedData);
    }
}

Restricting Fields

You can explicitly specify which fields should be mapped:

$allowedFields = ['uid', 'pid', 'header', 'bodytext'];
$mappedData = $this->tcaMapper->mapRecord('tt_content', $record, $allowedFields);

Mapping Multiple Records

Use mapRecords() for lists:

$records = $this->myRepository->findAll();
$mappedList = $this->tcaMapper->mapRecords('tx_myext_table', $records);

Advanced Features

Relation Resolution

The mapper can resolve relations automatically if a resolveDepth > 0 is provided:

$mappedData = $this->tcaMapper->mapRecord(
    'tx_myext_table',
    $record,
    resolveDepth: 1
);

Supported relations:

  • FAL: sys_file_reference (images, documents) are always resolved to an array of file objects.
  • 1:n: IRRE relations using foreign_field and optional foreign_table_field.
  • M:M: Relations using an MM table.
  • Select/Group: Fields with a foreign_table and comma-separated UIDs.

Field Configuration

You can pass a fieldConfiguration to control which fields are returned for specific tables, which is especially useful for nested relations:

$fieldConfiguration = [
    'tx_myext_table' => [
        'allowed' => ['uid', 'title', 'related_item']
    ],
    'tx_myext_related' => [
        'excluded' => ['internal_field']
    ]
];

$mappedData = $this->tcaMapper->mapRecord(
    'tx_myext_table',
    $record,
    fieldConfiguration: $fieldConfiguration,
    resolveDepth: 1
);

Custom Callbacks

Use customCallbacks to handle computed fields or modify values dynamically:

$callbacks = [
    'full_name' => function (array $record, array $mappedRecord) {
        return $record['first_name'] . ' ' . $record['last_name'];
    },
    'dynamic_link' => function (array $record) {
        return 'https://example.com/' . $record['slug'];
    }
];

$mappedData = $this->tcaMapper->mapRecord(
    'tx_myext_table',
    $record,
    customCallbacks: $callbacks
);

Field Renaming

Rename database fields for the API output:

$renamedFields = [
    'tx_myext_legacy_field' => 'new_api_name'
];

$mappedData = $this->tcaMapper->mapRecord(
    'tx_myext_table',
    $record,
    renamedFields: $renamedFields
);

Auto-CRUD Resources

sg_apicore allows you to expose TYPO3 database tables as API resources with full CRUD (Create, Read, Update, Delete) support without writing any controller code.

Resource Registration

Resources are registered in your extension's ext_localconf.php via the ResourceRegistry service.

use SGalinski\SgApiCore\Service\ResourceRegistry;use TYPO3\CMS\Core\Utility\GeneralUtility;

$resourceRegistry = GeneralUtility::makeInstance(ResourceRegistry::class);

// Register tt_content as a resource for the 'public' API
$resourceRegistry->registerResource('public', 'tt_content', '/contents', [
    'allowedOperations' => ['list', 'get'],
    'readFields' => ['uid', 'pid', 'header', 'bodytext']
]);

// Register a custom table for the 'partner' API with full CRUD and scopes
$resourceRegistry->registerResource('partner', 'tx_myext_domain_model_item', '/items', [
    'allowedOperations' => ['list', 'get', 'create', 'update', 'delete'],
    'writeFields' => ['header', 'bodytext', 'pid'],
    'deleteMode' => 'hard',
    'tags' => ['Items'],
    'requiredScopes' => [
        'list' => ['partner:read'],
        'get' => ['partner:read'],
        'create' => ['partner:write'],
        'update' => ['partner:write'],
        'delete' => ['partner:write'],
    ]
]);
// Register pages as a resource for the 'public' API
$resourceRegistry->registerResource('public', 'pages', '/pages', [
    'allowedOperations' => ['list', 'get'],
    'readFields' => ['uid', 'pid', 'title', 'doktype', 'slug']
]);

Configuration Options

  • table: The TYPO3 table name.
  • basePath: The base path for the resource endpoints (e.g., /items).
  • idField: The field used to identify a single item (default: uid).
  • allowedOperations: Array of enabled operations (list, get, create, update, delete).
  • readFields: Whitelist of fields for output mapping (empty = all except internal fields).
  • writeFields: Whitelist of fields accepted for create and update.
  • fieldConfiguration: Map of table names to their field configurations. Allows controlling allowed and excluded fields for both the main record and related records (when resolved).
    • Example:
      'fieldConfiguration' => [
          'tx_myext_domain_model_item' => [
              'allowed' => ['uid', 'header', 'related_item'],
          ],
          'tx_myext_domain_model_related' => [
              'excluded' => ['internal_secret'],
          ]
      ]
      
  • deleteMode: soft (default) uses DataHandler delete, hard deletes the DB record directly (no TYPO3 audit log).
  • rateLimit: Optional rate limit overrides for this resource (see RateLimiting.md).
  • requiredScopes: Associative array mapping operations to required scope arrays.
  • resolveDepth: Default recursion depth for resolving relations (default: 1).

If writeFields is empty, the OpenAPI request body is generated from readFields. If both are empty, the request body is generated from the table TCA (excluding uid).

Generated Endpoints

Based on the configuration, the following endpoints are automatically generated:

  • GET /api/{apiId}/v{version}/{basePath}: List resources (supports pagination, sorting, filtering).
  • GET /api/{apiId}/v{version}/{basePath}/{id}: Get a single resource.
  • POST /api/{apiId}/v{version}/{basePath}: Create a new resource.
  • PATCH /api/{apiId}/v{version}/{basePath}/{id}: Update an existing resource.
  • DELETE /api/{apiId}/v{version}/{basePath}/{id}: Delete a resource (returns 204 without a response body).

List Operation Features

Pagination

Use page and limit query parameters: GET /api/public/v1/contents?page=2&limit=20

Note: perPage is also supported as an alias for limit for backward compatibility.

Sorting

Use the sort query parameter. Prefix with - for descending order: GET /api/public/v1/contents?sort=header (ASC) GET /api/public/v1/contents?sort=-uid (DESC)

Filtering

Use the filter query parameter with field names: GET /api/public/v1/contents?filter[header]=Welcome

You can also use arrays for IN-queries: GET /api/public/v1/contents?filter[uid][]=1&filter[uid][]=2

Only fields defined in readFields (or whitelisted in fieldConfiguration or all if empty) can be used for filtering.

Persistence & DataHandler

For create, update, and delete operations, the extension uses the TYPO3 DataHandler. This ensures that:

  • All TYPO3 hooks are executed.
  • Reference indexing is updated.
  • History and logging are maintained.
  • Permissions are respected (if a backend user context exists).

Data provided in the request body is automatically mapped using the TcaMapper, handling types like booleans and dates correctly.

Backend User for Resource Writes

Auto-CRUD write operations (create, update, delete) can run under a dedicated backend user. Configure the backend user UID via the extension configuration key apiResourceWriteBackendUserId.

If set, the configured backend user's permissions and groups are used for resource write operations. If not set (or 0), the extension keeps the admin bypass behavior for write operations.

This setting only affects Auto-CRUD resource endpoints and does not apply to custom controllers.


OpenAPI Documentation

sg_apicore automatically generates OpenAPI 3.0 specifications based on the attributes in your controllers.

Browser Access

Every registered API provides endpoints for documentation:

  • JSON Specification: /api/{apiId}/v{version}/docs.json
  • Swagger UI: /api/{apiId}/v{version}/docs/ui

Example: https://your-website.com/api/public/v1/docs/ui

Metadata Attributes

To make the specification meaningful, use the following attributes on your controller methods:

  • #[ApiEndpoint]: Summary, description, and tags.
  • #[ApiQueryParam]: Describes a query parameter.
  • #[ApiBodyParam]: Describes fields in the JSON body.
  • #[ApiPathParam]: Describes a parameter in the URL path.
  • #[ApiResponse]: Describes possible responses (status code, description, schema).

Global Schemata

You can define global schemas that can be reused across multiple endpoints. This is useful for complex objects like " Offer" or "User".

use SGalinski\SgApiCore\Service\SchemaRegistry;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$schemaRegistry = GeneralUtility::makeInstance(SchemaRegistry::class);
$schemaRegistry->registerSchema('public', 'MyObject', [
    'type' => 'object',
    'properties' => [
        'id' => ['type' => 'integer'],
        'name' => ['type' => 'string']
    ]
], 'tx_my_table'); // Optional: Map to TCA table for enrichment

Referencing a schema in an attribute:

#[ApiResponse(status: 200, schema: 'MyObject')]
#[ApiResponse(status: 200, schema: 'MyObject[]')] // Array of objects

TCA Enrichment

sg_apicore automatically enriches schemas with labels from the TYPO3 TCA.

  1. Global Schemas: If a $tableName is provided during registerSchema(), all properties in the schema that match TCA columns will automatically receive the translated label as their OpenAPI description. This works recursively for nested objects with foreign_table relations.

  2. Ad-hoc Schemas: If you provide a schema name (table name) in the #[ApiResponse] attribute without a global definition, it will also be enriched.

  3. Merging with Examples: When combining a schema (reference or table) with an example, the generator merges the structures. Properties from the schema are preserved, and example values are used to infer types or provide sample data.

  4. Remapped Fields: If a property in your schema does not match the TCA column name (e.g., it was remapped in your API), you can add a custom x-tca-field property to the schema property definition to specify the original TCA column name.

    Example:

    $schemaRegistry->registerSchema('partner', 'Offer', [
        'type' => 'object',
        'properties' => [
            'tags' => [
                'type' => 'array',
                'x-tca-field' => 'offertags', // Original TCA column name
                'items' => [...]
            ]
        ]
    ], 'tx_citypower_domain_model_offer');
    

Automatic Schema Generation

sg_apicore can automatically generate schemas from your example data. If you provide an example in the #[ApiResponse] attribute, the service will recursively build an OpenAPI schema based on its structure and data types.

If you also provide a schema name (usually a table name or a DTO class name) in the #[ApiResponse] attribute, the generator will attempt to enrich the schema with descriptions from the corresponding TCA labels.

Example:

#[ApiResponse(status: 200, description: 'Success response', schema: 'tx_my_table', example: [
    'title' => 'Sample Title',
    'child' => [
        'name' => 'Sub-item'
    ]
])]

In this case, the generator will check tx_my_table for the title label and use it as a description. For the nested child object, it will look at the foreign_table configuration in the TCA of tx_my_table to resolve labels for the child's properties.

Schema Placeholders in Examples

If you provide an example in the #[ApiResponse] attribute, you can use placeholders to reference global schemas within your example structure. This is extremely useful for paginated responses where you want to show the full structure of the items without repeating the schema definition.

The format is schema:SchemaName or schema:SchemaName[] (for an array).

Example:

#[ApiResponse(status: 200, description: 'List of offers', schema: 'Offer[]', example: [
    'data' => 'schema:Offer[]',
    'meta' => [
        'total' => 100,
        'page' => 1
    ]
])]

The generator will automatically replace the placeholder with a generated stub based on the "Offer" schema's properties and their example values or types.

CLI Export

You can also export the specification to a file via the command line:

vendor/bin/typo3 api:openapi:generate --api=public --api-version=1 --out=openapi.json

Security Schemes

The generated specification automatically includes a bearerAuth scheme. If an API does not run in public mode, this scheme is marked as required globally for all endpoints.


Authentication & Scopes

sg_apicore provides a flexible system for authentication and authorization.

Authentication Modes

The default mode is defined per API in the ApiRegistry as a string (see APIs.md). Individual endpoints can override or extend this using the #[ApiRoute] attribute (supporting both string and array).

  1. Public: No authentication required.
  2. Token (Opaque Bearer): Requires an API key in the header: Authorization: Bearer <token>.
  3. User: Requires a user login. Supports access and refresh tokens.

Example for an endpoint that is both public and user-accessible:

#[ApiRoute(path: '/auth/login', methods: ['POST'], authMode: ['public', 'user'])]

Scopes (Permissions)

Scopes are used to control access to specific endpoints in a fine-grained manner. A token can have a list of scopes.

Enforcing Scopes

Use the #[RequireScopes] attribute on your controller method:

use SGalinski\SgApiCore\Attribute\RequireScopes;

#[RequireScopes(['partner:read', 'partner:write'])]
public function updateAction(...) {
    // This method will only be executed if the token possesses BOTH scopes.
}

User Authentication

If the user mode is active, a TYPO3 frontend user can authenticate via the login endpoint:

  • URL: POST /api/{apiId}/v1/auth/login
  • Body: {"username": "...", "password": "..."}
  • Response: Contains access_token, refresh_token, token_type, expires_in.

User Storage and Site Awareness

By default, users are searched for in the current TYPO3 Site Root. You can customize the storage pages (PIDs) in several ways:

  1. sg_account Integration: If sg_account is installed, it uses the storage defined in the Main Configuration.
  2. Site Configuration: Define storage PIDs in your site.yaml:
    apicore:
      userStoragePids: "123,456"
    
  3. Fallback: Falls back to the root page ID of the current site.

RequireUser Attribute

To ensure that a request comes from a real user (and not just an M2M token), use:

use SGalinski\SgApiCore\Attribute\RequireUser;

#[RequireUser]
public function profileAction(...) {
    // Only accessible to logged-in frontend users.
}

Events

The extension provides PSR-14 events to hook into various processes.

AfterUserAuthenticationEvent

This event is triggered after a user has successfully authenticated (e.g., password check passed), but before tokens are generated and the response is sent. It allows you to perform additional checks (like account expiration) and block the login.

  • Payload: getUser() (array), getTenantContext() (?TenantContext).
  • Blocking Login: Throw an SGalinski\SgApiCore\Exception\AuthenticationException within your listener to abort the login process with a custom message.

Example Listener:

public function __invoke(AfterUserAuthenticationEvent $event): void {
    $user = $event->getUser();
    if ($user['is_blocked']) {
        throw new AuthenticationException('Your account is blocked.');
    }
}

Authentication Error Responses

Authentication failures return RFC 7807 Problem JSON, include requestId for tracing, and set X-Request-ID. If rate limiting is applied, use X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers and optionally provide a rateLimit object in the response body.

  • 401 Unauthorized: Missing credentials (Authentication required.).
  • 401 Unauthorized: Invalid or expired token (Invalid or expired token.).
  • 403 Forbidden: Authenticated but missing required scopes or #[RequireUser].

Rate Limit Configuration

Use the extension configuration to enable and tune rate limits:

  • rateLimitEnabled (boolean)
  • rateLimitDefaultLimit (int, requests per window)
  • rateLimitWindowSeconds (int, window size)

See also: docs/RateLimiting.md.

Authentication Error Responses

Authentication failures return RFC 7807 Problem JSON, include requestId for tracing, and set X-Request-ID. If rate limiting is applied, use X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers and optionally provide a rateLimit object in the response body.

  • 401 Unauthorized: Missing credentials (Authentication required.).
  • 401 Unauthorized: Invalid or expired token (Invalid or expired token.).
  • 403 Forbidden: Authenticated but missing required scopes or #[RequireUser].

Token Management in the Backend

In the TYPO3 backend under System > API Core, you can:

  • Create new Opaque tokens (M2M).
  • Optionally bind tokens to a specific FE user (fe_users.uid via user_id).
  • View and revoke existing tokens.
  • Manage scopes and expiration dates.
  • Keep and reuse the current token filters while performing token actions.

Logging

sg_apicore offers a comprehensive logging system for API requests and responses. It is designed to ensure traceability without compromising sensitive data.

Global Configuration

Logging can be controlled in the extension configuration (settings.php or Extension Manager):

  • enableLogging: Global switch for API logging.
  • logHeaders: Whether HTTP headers should be logged.
  • logBody: Whether the request body should be logged.
  • logResponse: Whether the response body should be logged.
  • redactKeys: Comma-separated list of keys whose values are masked in the logs (e.g., password,token,access_token).

Per-Endpoint Configuration

With the #[ApiLogging] attribute, the global settings can be overridden for individual methods:

use SGalinski\SgApiCore\Attribute\ApiLogging;

#[ApiLogging(
    enableLogging: true,
    logHeaders: true,
    logBody: true,
    logResponse: true
)]
public function sensitiveAction(...) { ... }

Request Tracking

Every API request receives a unique Request ID (Correlation ID). This ID:

  1. Is attached to the request as the api.requestId attribute.
  2. Is included in every log message.
  3. Is returned as an X-Request-ID HTTP header in the response.

This allows an API call to be tracked from the client deep into the server logs.

Customizing the Log Destination

By default, logs are written to the file var/log/sg_apicore.log. This can be adjusted via the TYPO3 log configuration:

$GLOBALS['TYPO3_CONF_VARS']['LOG']['SGalinski']['SgApiCore']['Service']['LogService']['writerConfiguration'] = [
    \Psr\Log\LogLevel::INFO => [
        \TYPO3\CMS\Core\Log\Writer\FileWriter::class => [
            'logFile' => \TYPO3\CMS\Core\Core\Environment::getVarPath() . '/log/custom_api.log'
        ]
    ]
];

Tenants

In sg_apicore, every request is executed within a TenantContext. This enables the operation of multiple tenants ( e.g., different websites or departments) within a single TYPO3 instance.

Tenant Resolution

By default, the tenant is automatically derived from the TYPO3 Site. The TenantResolverChain successively checks various strategies:

  1. SiteTenantResolver: Uses the TYPO3 site configuration.
  2. HeaderTenantResolver: Checks for the X-Tenant-Id HTTP header.

Configuration

The strategy of the SiteTenantResolver can be adjusted in the extension configuration (settings.php or Extension Manager):

  • siteTenantIdSource:
    • identifier (default): Uses the site identifier from TYPO3 (e.g., main_site).
    • baseHost: Uses the site's hostname (e.g., www.example.com).
    • rootPageId: Uses the UID of the root page.
  • onMissingTenant: HTTP status code when no tenant is found (default: 404).

Usage in Code

The TenantContext is available in the request object as an attribute:

use SGalinski\SgApiCore\Context\TenantContext;

public function myAction(ServerRequestInterface $request): ResponseInterface {
    /** @var TenantContext $tenantContext */
    $tenantContext = $request->getAttribute('api.tenant');
    $tenantId = $tenantContext->getTenantId();

    // Access the TYPO3 site object (if available)
    $site = $tenantContext->getSite();
    // ...
}

Endpoint Filtering by Tenant

You can restrict specific endpoints to certain tenants using the tenants property of the #[ApiRoute] attribute. This is useful for extending an existing API for a specific tenant without affecting others.

use SGalinski\SgApiCore\Attribute\ApiRoute;

// Only available for the tenant 'citypower-tenant'
#[ApiRoute(path: '/custom-data', methods: ['GET'], tenants: 'citypower-tenant')]
public function customDataAction(): ResponseInterface {
    // ...
}

// Available for multiple specific tenants
#[ApiRoute(path: '/shared-data', methods: ['GET'], tenants: ['tenant-a', 'tenant-b'])]
public function sharedAction(): ResponseInterface {
    // ...
}

Multi-Language Handling

The TenantContext also captures the current language. By default, sg_apicore is fully language-aware:

  1. Language Resolution: The language is automatically detected via the TYPO3 Site configuration (e.g., via URL prefixes like /en/api/...).
  2. Context Initialization: The extension automatically initializes the TYPO3 LanguageAspect in the global Context. This ensures that Repositories and the TcaMapper automatically return translated content.
  3. Usage:
    $languageId = $tenantContext->getLanguageId();
    

Custom Resolvers

You can implement your own resolvers by implementing the TenantResolverInterface and registering the service with the sg_apicore.tenant_resolver tag.


Migration Guide: sg_rest to sg_apicore

This document describes how to migrate existing APIs from the deprecated sg_rest extension to the new sg_apicore architecture.

0. Preparation

To enable the backward compatibility features, you must first activate the legacy support in the extension configuration of sg_apicore:

  1. Go to Admin Tools > Extension Configuration.
  2. Select sg_apicore.
  3. Enable the checkbox Activate Legacy Support.
  4. Clear all caches.

1. Concept Comparison

Feature sg_rest sg_apicore
Routing Automatic via apiKey and entity Declarative via #[ApiRoute] attributes
Auth fe_users (tx_sgrest_auth_token) Opaque Tokens, JWT, Legacy Bridge
Models Extbase Models Plain PHP/DTOs or directly via TCA mapping
Response Fixed JSON format Flexible, Default: RFC 7807 (Errors)

2. Backward Compatibility (Legacy Mode)

sg_apicore provides a bridge to continue serving old clients with minimal changes.

URLs & Routing

The LegacyRoutingMiddleware automatically handles old sg_rest style requests. It supports two formats:

  1. Query-based: /?type=1595576052&tx_sgrest[request]=apiKey/entity/identifier
  2. Path-based: /apiKey/entity/identifier (Requires type=1595576052 parameter for precision)

These requests are internally mapped to the new structure: /api/legacy/v1/{apiKey}/{entity}/{identifier}

Important: You must register a legacy API in your ext_localconf.php to handle these requests. By default, the sg_apicore demo configuration already includes this.

Token Authentication (Bearer Token)

The old sg_rest authentication via authentication/authentication/getBearerToken is supported through a special mapping to /api/legacy/v1/auth/login.

Response Format: It returns the expected {"bearerToken": "..."} format.

Manual Endpoint Mapping

Since sg_apicore uses declarative routing, you must manually map your old endpoints in your controllers.

  1. Register the legacy API.
  2. Create or update your controllers.
  3. Add #[ApiRoute] with the path matching your old structure including the apiKey.

Example: Old endpoint: apiKey/news/list New mapping:

#[ApiRoute(path: '/apiKey/news/list', methods: ['GET'])]
public function listAction(...)

User Token Migration

The old tx_sgrest_auth_token in fe_users is deprecated and no longer supported. All clients MUST migrate to the new token system.

If you want to log in against a user account, you can use the following steps:

  1. Users should authenticate via the new /api/legacy/v1/auth/legacyLogin (or /api/legacy/v1/auth/login) endpoint.
  2. This will issue a new token (Opaque or JWT) stored in the tx_apicore_token table.
  3. Once all clients are migrated, the tx_sgrest_auth_token column can be removed from the fe_users table.

Response Format

Use the #[ApiLegacyMode] attribute on your controller or action to emulate the old JSON format (data wrapping, legacy error format). If your endpoint requires full TypoScript for rendering, use the #[RequireFullTypoScript] attribute as well:

#[ApiLegacyMode(source: 'sg_rest', wrapData: true, legacyErrorFormat: true)]
#[RequireFullTypoScript]
class MyLegacyController {
    // ...
}

3. Step-by-Step Migration

  1. API Registration: Register a new API in ext_localconf.php using the name of your old API key.
  2. Create Controller: Create a new controller and use #[ApiRoute] to map the old paths.
  3. Data Access:
    • For simple CRUD operations, use TCA Mapping (Phase K).
    • For complex logic, inject your repositories or services into the controller.
  4. Auth: The LegacyTokenProvider was removed. You must issue new tokens via the login endpoints.

4. Example: News API Migration (EXT:sg_demo)

In the sg_demo extension, a legacy news API was migrated.

Old Controller (sg_rest):

  • Path: news/news/list
  • Authenticated via api.auth request attribute (replaces AuthenticationServiceInterface).

Migrated Controller (sg_apicore):

namespace SGalinski\SgDemo\Controller\Rest\News;

use SGalinski\SgApiCore\Attribute\ApiRoute;
use SGalinski\SgApiCore\Attribute\ApiLegacyMode;
use SGalinski\SgApiCore\Attribute\RequireUser;

class NewsController {
    #[ApiRoute(path: '/news/news/list', methods: ['GET'], apiId: 'legacy')]
    #[ApiLegacyMode]
    #[RequireUser]
    public function getListAction($request) {
        // ... (use NewsRepository to fetch data)
        return $this->responseService->createSuccessResponse($response);
    }
}

The LegacyRoutingMiddleware handles the incoming request:

  1. Client calls /?type=1595576052&tx_sgrest[request]=news/news/list.
  2. Middleware maps this to /api/legacy/v1/news/news/list.
  3. Router matches the route /news/news/list for the legacy API.
  4. #[ApiLegacyMode] ensures the response format matches what the client expects.
  5. #[RequireUser] ensures the client must provide a valid (legacy) token.