API Platform на Symfony: чек-лист перед продакшеном
Дефолтная конфигурация API Platform удобна для разработки и опасна в проде: открытая документация, N+1-запросы, отсутствие лимитов. Разбираем, что проверить перед релизом.

API Platform из коробки даёт вам работающий REST и GraphABPI за пару аннотаций — и это ловушка. Дефолтные настройки заточены под скорость разработки, а не под прод: пагинация с лимитом, который клиент может переопределить сам, сериализация всех полей сущности подряд, включённый по умолчанию профайлер и открытый Swagger UI. Ниже — чек-лист того, что ломается первым под реальной нагрузкой и на что смотреть перед выкатом. Материал под API Platform 3.x на Symfony 6.4/7.x.
Речь не про то, как поднять API — это вы уже сделали. Речь про разрыв между «работает на localhost» и «держит трафик и не отдаёт лишнего». Инвалидацию HTTP-кеша и настройку Mercure оставляю за скобками — это отдельные темы, здесь только то, что бьёт по каждому проекту.
Сериализация: N+1 и утечка полей
Две самые частые проблемы живут в сериализации. Первая — вы отдаёте больше, чем собирались. Если у сущности User есть поле passwordHash и вы не описали группы сериализации, оно уедет в JSON. API Platform сериализует всё, что видит нормализатор, если не ограничить его группами.
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GetCollection;
use Symfony\Component\Serializer\Annotation\Groups;
#[ApiResource(
operations: [new GetCollection()],
normalizationContext: ['groups' => ['user:read']],
denormalizationContext: ['groups' => ['user:write']],
)]
class User
{
#[Groups(['user:read'])]
private int $id;
#[Groups(['user:read', 'user:write'])]
private string $email;
// Без группы — не попадёт в ответ и не примется из запроса
private string $passwordHash;
}
Правило простое: белый список полей через группы, а не чёрный. Добавили новое поле в сущность — оно по умолчанию не утечёт, пока вы явно не повесите на него группу чтения.
Вторая проблема — N+1. Коллекция из 30 записей, у каждой связь ManyToOne на автора — и вы получаете 31 SQL-запрос. API Platform не читает мысли Doctrine: если в группу сериализации попала связанная сущность, её нужно подгрузить джойном.
Дефолтная пагинация плюс ленивая загрузка связей — это тихая мина. На тестовых данных из десяти строк вы её не увидите. На проде она даст сотни запросов на один вызов эндпоинта.
Лечится либо расширением запроса через QueryCollectionExtension, где вы добавляете нужные leftJoin и addSelect к QueryBuilder, либо аннотацией #[ApiFilter] с eager-загрузкой. API Platform умеет автоматически подтягивать связи (eager loading) — но проверьте, что api_platform.eager_loading.enabled у вас не выключен и что глубина max_joins покрывает ваши вложенности. Точные ключи конфига сверяйте с разделом Performance в документации вашей версии, они менялись между мажорами.
Пагинация и лимиты
По умолчанию клиент может запросить любую страницу и, в зависимости от конфигурации, повлиять на размер выборки. Три вещи, которые надо зафиксировать до релиза:
- Максимальный размер страницы. Иначе кто-то запросит
itemsPerPage=100000и положит базу. Задайтеmaximum_items_per_pageв конфиге ресурса. - Разрешено ли клиенту менять размер страницы вообще. Флаг
client_items_per_page— если он выключен, размер жёстко берётся из вашей настройки. - Тип пагинации. Для больших таблиц offset-пагинация деградирует:
OFFSET 500000заставит СУБД пролистать полмиллиона строк. Смотрите в сторону cursor-based пагинации для тяжёлых коллекций.
Проверьте это руками: отправьте запрос с заведомо большим itemsPerPage и посмотрите, обрежется он до вашего максимума или нет.
Что должно быть выключено в проде
Часть удобных для разработки вещей в проде — дыра или лишняя нагрузка.
| Что | В dev | В prod |
|---|---|---|
| Swagger UI / документация | Открыта | Закрыта или за авторизацией |
| Symfony Profiler / Web Debug Toolbar | Включён | Пакет только в require-dev |
| APP_ENV | dev | prod |
| APP_DEBUG | 1 | 0 |
| Composer | с dev-зависимостями | --no-dev --optimize-autoloader |
| Doctrine metadata / query cache | Может быть выключен | Прогрет, на APCu/Redis |
Отдельно про документацию: даже если вы не собираетесь публиковать API наружу, открытый /docs и /api раскрывают всю схему — какие есть сущности, поля, операции. Это подсказка атакующему, что искать. Если API внутренний — закройте маршруты документации на уровне nginx или через security в конфиге.
Безопасность операций
Каждая операция ресурса по умолчанию доступна всем. Нет глобального «запретить всё, разрешить точечно» — вы прописываете доступ явно на операции или ресурсе через атрибут security.
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\Post;
#[ApiResource(
operations: [
// Читать может любой залогиненный
new Get(security: "is_granted('ROLE_USER')"),
// Создавать — только админ
new Post(security: "is_granted('ROLE_ADMIN')"),
]
)]
class Invoice
{
// ...
}
Пройдитесь по списку всех ресурсов и для каждой операции задайте вопрос: кто это может вызвать. Забытая незащищённая Delete-операция — самая дорогая ошибка из возможных. Для проверки владельца ресурса (пользователь редактирует только свои записи) используйте securityPostDenormalize с доступом к object.
Валидация входных данных
Denormalization-группы решают, какие поля клиент может писать, но не проверяют содержимое. Без ограничений Symfony Validator в базу уедет пустая строка вместо email и отрицательное количество в заказе. Вешайте #[Assert\...] на поля и проверяйте, что группы валидации совпадают с денормализацией, если вы используете разные группы на create и update.
Rate limiting
API Platform сам по себе не ограничивает частоту запросов. Один клиент в цикле выест ваши ресурсы. Symfony даёт компонент symfony/rate-limiter — настройте лимит на чувствительные эндпоинты (логин, создание сущностей) через фабрику лимитеров и повесьте проверку в event listener или на уровне обратного прокси. На каком уровне резать — вопрос архитектуры, но резать надо.
Чек-лист перед выкатом
APP_ENV=prod,APP_DEBUG=0, кеш прогрет командойbin/console cache:warmup.- Composer установлен с
--no-dev --optimize-autoloader, профайлер и maker-bundle не в require. - У каждого ресурса заданы
normalizationContextиdenormalizationContextс группами; чувствительные поля не имеют группы чтения. - На всех операциях прописан
security; нет операций, доступных анонимно по недосмотру. - Задан
maximum_items_per_page; проверено запросом с большим лимитом. - Проверены N+1: включите Doctrine SQL logger на тестовом стенде и посмотрите число запросов на коллекционном эндпоинте.
- Валидация полей через Assert стоит на всех записываемых полях.
- Настроен rate limiting хотя бы на аутентификацию.
- Документация
/docsи/apiзакрыта, если API не публичный. - Doctrine metadata и query cache настроены на APCu или Redis, а не на дефолтный dev-драйвер.
Что мерить в проде
После выката смотрите на две вещи. Первое — количество SQL-запросов на запрос к API: если оно растёт линейно с размером коллекции, у вас остался N+1, который на тестовых данных был незаметен. Соберите это через Doctrine-метрики или APM вроде Blackfire. Второе — время ответа на коллекционных эндпоинтах при росте offset: резкая деградация на дальних страницах означает, что пора уходить с offset-пагинации на курсорную. Проверять конфиг лучше не глазами по документации, а тестом: напишите функциональный тест, который бьёт по эндпоинту без авторизации и ждёт 401 — тогда забытая незащищённая операция всплывёт на CI, а не на проде.
Тендерный специалист 44-ФЗ / 223-ФЗ: Разбор PDF-документации закупки, поиск «заточек» под поставщика, чек-лист заявки.
Спросить за 20 ₽Источники: API Platform — официальная документация, Symfony RateLimiter Component
Частые вопросы
У меня API Platform 2.7, а не 3.x — чек-лист подойдёт?
Как быстро найти N+1, не гадая?
Достаточно ли групп сериализации, чтобы скрыть поле с паролем?
Я всё настроил, но эндпоинт всё равно отдаёт лишние поля — почему?
Нужен ли rate limiting, если API за корпоративным VPN?
Как закрыть документацию, но оставить сам API работающим?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.