Development of AJAX controllers for 1C-Bitrix

Our company is engaged in the development, support and maintenance of Bitrix and Bitrix24 solutions of any complexity. From simple one-page sites to complex online stores, CRM systems with 1C and telephony integration. The experience of developers is confirmed by certificates from the vendor.
Our competencies:
Development stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1175
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Website development for FIXPER company
    811
  • image_bitrix-bitrix-24-1c_development_of_an_online_appointment_booking_widget_for_a_medical_center_594_0.webp
    Development based on Bitrix, Bitrix24, 1C for the company Development of an Online Appointment Booking Widget for a Medical Center
    564
  • image_bitrix-bitrix-24-1c_mirsanbel_458_0.webp
    Development based on 1C Enterprise for MIRSANBEL
    747
  • image_crm_dolbimby_434_0.webp
    Website development on CRM Bitrix24 for DOLBIMBY
    655
  • image_crm_technotorgcomplex_453_0.webp
    Development based on Bitrix24 for the company TECHNOTORGKOMPLEKS
    976

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.