Example: API endpoint management
A common pattern is storing API endpoints with their credentials in a database table. This example shows how to combine TCA vault fields with the HTTP client.
Table of contents
Step 1: Define the TCA table
EXT:my_extension/Configuration/TCA/tx_myext_apiendpoint.php
<?php
return [
'ctrl' => [
'title' => 'API Endpoints',
'label' => 'name',
],
'columns' => [
'name' => [
'label' => 'Name',
'config' => ['type' => 'input', 'required' => true],
],
'url' => [
'label' => 'API Base URL',
'config' => ['type' => 'input', 'required' => true],
],
'token' => [
'label' => 'API Token',
'config' => [
'type' => 'input',
'renderType' => 'vaultSecret',
],
],
],
];
Creating an API endpoint record (backend):
- Go to List module and select your storage folder
- Click + Create new record and select API Endpoint
-
Fill in the form:
- Name:
Stripe - API Base URL:
https://api.stripe.com/v1 - API Token: Paste your actual API key (stored securely in vault)
- Name:
- Click Save
The token field uses renderType: 'vaultSecret' which:
- Shows a masked password field with reveal/copy buttons
- Automatically stores the secret in the vault on save
- Stores only a UUID v7 reference in the database
What gets stored in the database:
Database content (token is UUID, not the secret)
SELECT uid, name, url, token FROM tx_myext_apiendpoint;
-- | uid | name | url | token |
-- |-----|--------|---------------------------|--------------------------------------|
-- | 1 | Stripe | https://api.stripe.com/v1 | 01937b6e-4b6c-7abc-8def-0123456789ab |
Copied!
Step 2: Create a DTO for type safety
EXT:my_extension/Classes/Domain/Dto/ApiEndpoint.php
<?php
declare(strict_types=1);
namespace MyVendor\MyExtension\Domain\Dto;
final readonly class ApiEndpoint
{
public function __construct(
public int $uid,
public string $name,
public string $url,
public string $token, // Contains vault UUID, not the secret
) {}
/**
* @param array<string, mixed> $row
*/
public static function fromDatabaseRow(array $row): self
{
return new self(
uid: (int) $row['uid'],
name: (string) $row['name'],
url: (string) $row['url'],
token: (string) $row['token'],
);
}
}
Step 3: Create a service for authenticated requests
EXT:my_extension/Classes/Service/ApiClientService.php
<?php
declare(strict_types=1);
namespace MyVendor\MyExtension\Service;
use MyVendor\MyExtension\Domain\Dto\ApiEndpoint;
use Netresearch\NrVault\Http\SecretPlacement;
use Netresearch\NrVault\Service\VaultServiceInterface;
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Core\Http\RequestFactory;
final class ApiClientService
{
public function __construct(
private readonly VaultServiceInterface $vault,
private readonly RequestFactory $requestFactory,
) {}
/**
* Call an API endpoint with vault-managed authentication.
*
* The token is retrieved from vault and injected at request time.
* It never appears in this code and is wiped from memory immediately.
*/
public function call(
ApiEndpoint $endpoint,
string $method,
string $path,
array $data = [],
): ResponseInterface {
// Create PSR-7 request using TYPO3's RequestFactory
$request = $this->requestFactory->createRequest(
$method,
rtrim($endpoint->url, '/') . '/' . ltrim($path, '/'),
);
if ($data !== [] && \in_array($method, ['POST', 'PUT', 'PATCH'], true)) {
$request = $request
->withHeader('Content-Type', 'application/json')
->withBody(
\GuzzleHttp\Psr7\Utils::streamFor(json_encode($data))
);
}
// Send via VaultHttpClient - token never exposed to application
return $this->vault->http()
->withAuthentication($endpoint->token, SecretPlacement::Bearer)
->withReason('API call to ' . $endpoint->name . ': ' . $path)
->sendRequest($request);
}
/**
* Convenience method for GET requests.
*/
public function get(ApiEndpoint $endpoint, string $path): array
{
$response = $this->call($endpoint, 'GET', $path);
return json_decode(
$response->getBody()->getContents(),
true,
512,
JSON_THROW_ON_ERROR,
);
}
}
Step 4: Use the service
Example controller or command
<?php
use MyVendor\MyExtension\Domain\Dto\ApiEndpoint;
use MyVendor\MyExtension\Service\ApiClientService;
// Load endpoint from database
$row = $connection->select(['*'], 'tx_myext_apiendpoint', ['uid' => 1])
->fetchAssociative();
$endpoint = ApiEndpoint::fromDatabaseRow($row);
// Make authenticated API call
$customers = $this->apiClientService->get($endpoint, '/customers');
What happens under the hood
- The
tokenfield contains a UUID v7 like01937b6e-4b6c-... Vaultretrieves the actual token from vaultHttp Client:: send Request () - Token is injected into the
Authorization: Bearer ...header sodium_immediately wipes the token from memorymemzero () - The HTTP call is logged to the audit trail (without the secret)
Security benefits
This pattern provides several security advantages:
- No secret exposure
- Application code never sees the actual API token. The DTO contains only the vault UUID, which is useless without vault access.
- Memory safety
- Secrets are cleared from memory immediately after injection using
sodium_.memzero () - Audit trail
- Every HTTP call is logged with the endpoint name, HTTP method, URL, and status code - but never the secret itself.
- Separation of concerns
- Credential management is handled by the vault. Application code focuses on business logic.
- No CLI or file access required
- Editors and admins can manage API endpoints entirely through the TYPO3 backend. The vault secret field provides a secure password input with reveal and copy functionality - no command line needed.