Developing AJAX Controllers for 1C-Bitrix
A Bitrix component displays a product list with a "Load More" button. On click, the entire page reloads — or the developer has wired up a jQuery request to /ajax.php?action=load_more that accepts POST data and returns HTML from the component's output buffer. This approach works, but it is archaic. Bitrix D7 provides controllers — \Bitrix\Main\Engine\Controller — which offer a structured way to create AJAX endpoints with authorization, CSRF protection, and a standard response format.
Why a D7 Controller Is Better Than a Custom ajax.php
A handwritten ajax.php is a concentration of risks: no centralized CSRF checking, no standard error handling, no automatic authorization, code scattered across files. A D7 controller solves all of this out of the box:
namespace MyVendor\Catalog\Controller;
use Bitrix\Main\Engine\Controller;
use Bitrix\Main\Engine\ActionFilter;
class Product extends Controller
{
public function configureActions(): array
{
return [
'getList' => [
'prefilters' => [
new ActionFilter\Authentication(), // authorization check
new ActionFilter\Csrf(), // CSRF token
],
],
];
}
public function getListAction(int $sectionId, int $page = 1): array
{
// Return an array — Bitrix wraps it in {"status":"success","data":{...}}
return [
'items' => $this->loadProducts($sectionId, $page),
'total' => $this->countProducts($sectionId),
];
}
}
The framework automatically wraps the return value in a JSON envelope {"status": "success", "data": {...}} or {"status": "error", "errors": [...]} when an exception is thrown.
Registering the Controller
The controller is registered in the module routing file routes.php:
// In the module .settings.php or via Application::getInstance()->getRouter()
\Bitrix\Main\Routing\RoutingConfigurator::registerRoutes(function($routes) {
$routes->post('/api/catalog/product/get-list/', 'MyVendor\Catalog\Controller\Product::getListAction');
});
An alternative — via the standard component mechanism: the controller is called through /bitrix/services/main/ajax.php?action=myvendor:catalog.product.getList.
Deep Dive: Filters and Middleware
The controller supports a chain of filters before and after action execution. This is analogous to middleware in Laravel.
Standard prefilters:
-
ActionFilter\Authentication— requires authorization -
ActionFilter\Csrf— validates the CSRF token (bitrix_sessid) -
ActionFilter\HttpMethod— restricts methods (GET/POST) -
ActionFilter\Scope— restricts context (web only, REST only, CLI only)
Custom filter — for example, checking permissions on a specific resource:
class ResourceAccessFilter extends \Bitrix\Main\Engine\ActionFilter\Base
{
public function onBeforeAction(Event $event): ?EventResult
{
$action = $event->getParameter('action');
$sectionId = $action->getController()->getRequest()->getPost('sectionId');
if (!SectionAccessChecker::canRead((int)$sectionId)) {
$this->addError(new \Bitrix\Main\Error('Access denied', 403));
return new EventResult(EventResult::ERROR, null, null, $this);
}
return null;
}
}
Handling Files and Complex Data
The controller automatically deserializes incoming data. For file uploads — via $this->getRequest()->getFile('image'), returning an \Bitrix\Main\Web\Upload\UploadedFile object with type and size validation methods.
For pagination, the built-in \Bitrix\Main\Engine\Response\DataType\Page object is used:
public function getListAction(int $page): \Bitrix\Main\Engine\Response\DataType\Page
{
$pageSize = 20;
$items = ProductTable::getList([
'limit' => $pageSize,
'offset' => ($page - 1) * $pageSize,
])->fetchAll();
return new \Bitrix\Main\Engine\Response\DataType\Page('items', $items, fn() => ProductTable::getCount());
}
Testing Controllers
D7 controllers can be tested via PHPUnit without an HTTP request: a controller instance is created, the Request is substituted, and run() is called. This is a major advantage over handwritten ajax.php files, which are difficult to test.
Development Timeline
| Scale | Scope | Timeline |
|---|---|---|
| Basic | 5–10 endpoints + standard filters | 1–2 weeks |
| Medium | + custom filters + file upload + rate limiting | 3–4 weeks |
| Extended | + versioning + OpenAPI documentation + tests | 5–7 weeks |
If the project already has handwritten ajax.php files, they should be migrated to controllers gradually, starting with the most heavily loaded or problematic endpoints.







