Ajax in the backend

An Ajax endpoint in the TYPO3 backend is usually implemented as a method in a regular controller. The method receives a request object implementing the \Psr\Http\Message\ServerRequestInterface , which allows to access all aspects of the requests and returns an appropriate response in a normalized way. This approach is standardized as PSR-7.

Create a controller

By convention, a controller is placed within the extension's Controller/ directory, optionally in a subdirectory. To have such controller, create a new ExampleController in Classes/Controller/ExampleController.php inside your extension.

The controller needs not that much logic right now. We create a method called doSomethingAction() which will be our Ajax endpoint.

EXT:my_extension/Classes/Controller/ExampleController.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

final class ExampleController
{
    public function doSomethingAction(ServerRequestInterface $request): ResponseInterface
    {
        // TODO: return ResponseInterface
    }
}
Copied!

In its current state, the method does nothing yet. We can add a very generic handling that exponentiates an incoming number by 2. The incoming value will be passed as a query string argument named input.

EXT:my_extension/Classes/Controller/ExampleController.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

final class ExampleController
{
    public function doSomethingAction(ServerRequestInterface $request): ResponseInterface
    {
        $input = $request->getQueryParams()['input']
            ?? throw new \InvalidArgumentException(
                'Please provide a number',
                1580585107,
            );

        $result = $input ** 2;

        // TODO: return ResponseInterface
    }
}
Copied!

We have computed our result by using the exponentiation operator, but we do nothing with it yet. It is time to build a proper response:

EXT:my_extension/Classes/Controller/ExampleController.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

final class ExampleController
{
    public function __construct(
        private readonly ResponseFactoryInterface $responseFactory,
    ) {}

    public function doSomethingAction(ServerRequestInterface $request): ResponseInterface
    {
        $input = $request->getQueryParams()['input']
            ?? throw new \InvalidArgumentException(
                'Please provide a number',
                1580585107,
            );

        $result = $input ** 2;

        $response = $this->responseFactory->createResponse()
            ->withHeader('Content-Type', 'application/json; charset=utf-8');
        $response->getBody()->write(
            json_encode(['result' => $result], JSON_THROW_ON_ERROR),
        );
        return $response;
    }
}
Copied!

Register the endpoint

The endpoint must be registered as route. Create a file called Configuration/Backend/AjaxRoutes.php in your extension. The file basically just returns an array of route definitions. Every route in this file will be exposed to JavaScript automatically. Let us register our endpoint now:

EXT:my_extension/Configuration/Backend/AjaxRoutes.php
<?php

use MyVendor\MyExtension\Controller\ExampleController;

return [
    'myextension_example_dosomething' => [
        'path' => '/my-extension/example/do-something',
        'target' => ExampleController::class . '::doSomethingAction',
    ],
];
Copied!

The naming of the key myextension_example_dosomething and path /my-extension/example/do-something are up to you, but should contain the extension name, controller name and action name to avoid potential conflicts with other existing routes.

Protect the endpoint

Make sure to protect your endpoint against unauthorized access, if it performs actions which are limited to authorized backend users only.

Inherit access from backend module

New in version 12.4.37 / 13.4.18

This functionality was introduced in response to security advisory TYPO3-CORE-SA-2025-021 to mitigate broken access control in backend AJAX routes.

If your endpoint is part of a backend module, you can configure your endpoint to inherit access rights from this specific module by using the configuration option inheritAccessFromModule:

EXT:my_extension/Configuration/Backend/AjaxRoutes.php
<?php

use MyVendor\MyExtension\Controller\ExampleController;

return [
    'myextension_example_dosomething' => [
        'path' => '/my-extension/example/do-something',
        'target' => ExampleController::class . '::doSomethingAction',
        'inheritAccessFromModule' => 'my_module',
    ],
];
Copied!

Use permission checks on standalone endpoints

In case you're providing a standalone endpoint (that is, the endpoint is not bound to a specific backend module), make sure to perform proper permission checks on your own. You can use the backend user object to perform various authorization and permission checks on incoming requests.

Use in Ajax

Since the route is registered in AjaxRoutes.php it is exposed to JavaScript now and stored in the global TYPO3.settings.ajaxUrls object identified by the used key in the registration. In this example it is TYPO3.settings.ajaxUrls.myextension_example_dosomething.

Now you are free to use the endpoint in any of your Ajax calls. To complete this example, we will ask the server to compute our input and write the result into the console.

EXT:my_extension/Resources/Public/JavaScript/Calculate.js
import AjaxRequest from "@typo3/core/ajax/ajax-request.js";

// Generate a random number between 1 and 32
const randomNumber = Math.ceil(Math.random() * 32);
new AjaxRequest(TYPO3.settings.ajaxUrls.myextension_example_dosomething)
  .withQueryArguments({input: randomNumber})
  .get()
  .then(async function (response) {
    const resolved = await response.resolve();
    console.log(resolved.result);
  });
Copied!