Стройте ответ вокруг владения состоянием, лимитов, отказа и восстановления. Определение становится инженерным ответом только в конкретном production-сценарии.
Вопросы и ответы
36 подробных ответов
011. Что такое HTTP и как устроен цикл запрос-ответ?
junior
Короткий ответ: HTTP (HyperText Transfer Protocol) — текстовый прикладной протокол клиент-сервер поверх TCP (в HTTP/3 — поверх QUIC/UDP). Клиент отправляет запрос, сервер возвращает ответ; соединение без сохранения состояния между запросами (stateless).
Подробно:
Запрос состоит из стартовой строки (метод + путь + версия), заголовков и опционального тела:
POST /api/v1/users HTTP/1.1
Host: api.example.com
Content-Type: application/json
Accept: application/json
Content-Length: 38
{"name": "Анна", "email": "a@ex.com"}
Ответ состоит из строки статуса (версия + код + reason phrase), заголовков и тела:
HTTP/1.1 201 Created
Content-Type: application/json
Location: /api/v1/users/42
{"id": 42, "name": "Анна", "email": "a@ex.com"}
⚠️ Ловушка: «HTTP работает только поверх TCP» — неверно для HTTP/3, который использует QUIC поверх UDP. И reason phrase («OK», «Created») носит чисто информативный характер — клиенты должны опираться на числовой код, а не на текст.
022. HTTP-методы: семантика, safe и идемпотентные
junior
Короткий ответ: Метод задаёт намерение операции над ресурсом. Safe — не меняет состояние сервера (только чтение). Idempotent — повторный одинаковый запрос даёт тот же эффект, что и одиночный.
Подробно:
| Метод | Назначение | Safe | Idempotent | Тело запроса | Тело ответа |
|---|---|---|---|---|---|
| GET | Получить ресурс | ✅ | ✅ | нет | да |
| HEAD | Как GET, но только заголовки | ✅ | ✅ | нет | нет |
| OPTIONS | Узнать допустимые методы / CORS | ✅ | ✅ | нет | да |
| POST | Создать / выполнить действие | ❌ | ❌ | да | да |
| PUT | Полностью заменить / создать по URI | ❌ | ✅ | да | да |
| PATCH | Частично изменить ресурс | ❌ | ❌* | да | да |
| DELETE | Удалить ресурс | ❌ | ✅ | опц. | опц. |
* PATCH идемпотентен не по спецификации, но может быть таковым при правильном проектировании (например, замена полей абсолютными значениями вместо инкрементов).
- Safe-методы (GET, HEAD, OPTIONS) можно кэшировать, префетчить, повторять без последствий.
- Idempotent-методы (GET, HEAD, OPTIONS, PUT, DELETE) безопасно ретраить при сетевых сбоях.
- HEAD полезен, чтобы проверить наличие ресурса или его размер (
Content-Length) без скачивания тела. - OPTIONS используется браузером для CORS-preflight.
⚠️ Ловушка: «GET всегда без тела» — спецификация HTTP/1.1 разрешает тело у GET, но его поведение не определено, многие прокси и серверы его игнорируют или режут соединение. Не передавайте параметры в теле GET. Также: safe не равно idempotent — DELETE идемпотентен, но не безопасен.
033. В чём разница между POST, PUT и PATCH?
junior
Короткий ответ: POST создаёт ресурс (сервер назначает URI) и не идемпотентен; PUT полностью заменяет ресурс по известному URI и идемпотентен; PATCH частично обновляет ресурс.
Подробно:
POST — создание подчинённого ресурса в коллекции, URI назначает сервер:
POST /api/v1/articles HTTP/1.1
Content-Type: application/json
{"title": "REST", "body": "..."}
HTTP/1.1 201 Created
Location: /api/v1/articles/777
Два одинаковых POST создадут две статьи — не идемпотентно.
PUT — клиент знает URI и присылает полное представление; ресурс создаётся или заменяется целиком:
PUT /api/v1/articles/777 HTTP/1.1
Content-Type: application/json
{"title": "REST v2", "body": "новый текст", "tags": []}
Повторный идентичный PUT оставит ресурс в том же состоянии — идемпотентно. Важно: поля, не указанные в теле, считаются обнулёнными/удалёнными (полная замена).
PATCH — частичное изменение, передаются только меняемые поля:
PATCH /api/v1/articles/777 HTTP/1.1
Content-Type: application/json
{"title": "REST v3"}
Поле body остаётся прежним. Существует формализованный application/json-patch+json (RFC 6902) с операциями add/remove/replace.
⚠️ Ловушка: частая ошибка — использовать PUT для частичного обновления. Если PUT принимает частичное тело и не зануляет остальные поля, он семантически превращается в PATCH и нарушает контракт. Если клиент шлёт PUT без поля — оно должно исчезнуть, иначе это нарушение семантики.
044. Что такое идемпотентность и зачем она нужна?
middle
Короткий ответ: Операция идемпотентна, если выполнение её N раз даёт тот же эффект на состоянии сервера, что и одно выполнение. Это позволяет безопасно повторять запросы при сетевых сбоях.
Подробно:
Идемпотентность касается эффекта на сервере, а не идентичности ответов. Например, два DELETE одного ресурса: первый вернёт 204, второй — 404, но состояние сервера (ресурс отсутствует) одинаково — это идемпотентно.
Идемпотентные методы: GET, HEAD, OPTIONS, PUT, DELETE.
Не идемпотентен по умолчанию: POST (создаёт новую сущность при каждом вызове), PATCH (зависит от семантики — инкремент balance += 10 не идемпотентен, замена balance = 100 идемпотентна).
Зачем: при таймауте клиент не знает, дошёл запрос или нет. Идемпотентный запрос можно безопасно повторить. Неидемпотентный (POST оплаты) повторять опасно — можно создать дубликат.
⚠️ Ловушка: идемпотентность не про «возвращается одинаковый ответ». Счётчик просмотров, реализованный на GET (GET /article?inc_views=1), нарушает контракт: GET должен быть safe и idempotent, а тут он меняет состояние. Поисковики и префетч-агенты «накрутят» просмотры.
055. Idempotency key для платежей
middle
Короткий ответ: Idempotency-Key — уникальный идентификатор, который клиент передаёт в заголовке, чтобы сервер распознал повтор неидемпотентного запроса (например POST оплаты) и не выполнил операцию дважды.
Подробно:
POST создаёт ресурс — он не идемпотентен. Но при платежах ретрай после таймаута не должен списывать деньги дважды. Решение — клиент генерирует уникальный ключ (UUID) и шлёт его:
POST /api/v1/payments HTTP/1.1
Idempotency-Key: 7c4e1f2a-1d3b-4f5a-9c8e-6b2a1d0e3f4c
Content-Type: application/json
{"amount": 5000, "currency": "RUB", "order_id": "A-123"}
Логика сервера:
- При первом запросе с этим ключом — выполнить операцию, сохранить (ключ → результат + статус) в хранилище (например Redis с TTL).
- При повторе с тем же ключом — не выполнять снова, а вернуть сохранённый результат (обычно тот же 201 и то же тело).
- Если первый запрос ещё в процессе (in-flight) — вернуть 409 Conflict или подождать.
- Если тело при повторе отличается от исходного — вернуть 422 (ключ переиспользован с другими данными).
Так делают Stripe, PayPal, YooKassa. Ключ хранят с TTL (например 24 часа).
⚠️ Ловушка: ключ должен генерировать клиент (до отправки), а не сервер, иначе при ретрае сгенерируется новый ключ и защита не сработает. Также важна атомарность: проверка «есть ли ключ» и запись должны быть транзакционными (через INSERT ... ON CONFLICT или Redis SETNX), иначе две параллельные попытки оба пройдут проверку и спишут дважды.
066. Коды статусов HTTP: классы 1xx-5xx
junior
Короткий ответ: Первая цифра кода задаёт класс: 1xx — информационные, 2xx — успех, 3xx — перенаправление, 4xx — ошибка клиента, 5xx — ошибка сервера.
Подробно:
- 1xx Informational — промежуточные. 100 Continue (можно слать тело), 101 Switching Protocols (апгрейд до WebSocket).
- 2xx Success — 200 OK, 201 Created, 202 Accepted (принято на асинхронную обработку), 204 No Content, 206 Partial Content (range-запросы).
- 3xx Redirection — 301 Moved Permanently, 302 Found, 303 See Other, 304 Not Modified, 307/308 (сохраняют метод).
- 4xx Client Error — 400, 401, 403, 404, 405 Method Not Allowed, 409 Conflict, 410 Gone, 422 Unprocessable Entity, 429 Too Many Requests.
- 5xx Server Error — 500 Internal Server Error, 501 Not Implemented, 502 Bad Gateway, 503 Service Unavailable, 504 Gateway Timeout.
⚠️ Ловушка: возвращать 200 OK с телом {"error": "..."} — антипаттерн (так делают плохие SOAP/legacy API). Клиенты и мониторинг ориентируются на код статуса; ошибка должна иметь 4xx/5xx, иначе сбои «прячутся» под успешным кодом.
077. Ключевые коды статусов и когда их использовать
junior
Короткий ответ: Нужно знать минимум 200/201/204, 301/302/304, 400/401/403/404/409/422/429, 500/502/503 и их семантику.
Подробно:
| Код | Когда |
|---|---|
| 200 OK | Успешный GET/PUT/PATCH с телом |
| 201 Created | Ресурс создан (обычно после POST), желателен Location |
| 204 No Content | Успех без тела (DELETE, PUT без возврата) |
| 301 Moved Permanently | Ресурс навсегда по новому URL (кэшируется, влияет на SEO) |
| 302 Found | Временное перенаправление |
| 304 Not Modified | Кэш клиента актуален (ответ на условный GET) |
| 400 Bad Request | Некорректный синтаксис/невалидный JSON, битый запрос |
| 401 Unauthorized | Не аутентифицирован (нет/невалидны креды) |
| 403 Forbidden | Аутентифицирован, но нет прав |
| 404 Not Found | Ресурс не найден |
| 409 Conflict | Конфликт состояния (дубликат, конкурентное изменение) |
| 422 Unprocessable Entity | Синтаксис ок, но семантически невалидно (валидация бизнес-правил) |
| 429 Too Many Requests | Превышен rate limit |
| 500 Internal Server Error | Необработанное исключение на сервере |
| 502 Bad Gateway | Апстрим вернул некорректный ответ (проблема прокси/сервиса) |
| 503 Service Unavailable | Сервис временно недоступен (перегрузка/maintenance), желателен Retry-After |
⚠️ Ловушка: 502 vs 503 vs 504. 502 — прокси получил битый ответ от апстрима; 503 — сам сервис недоступен/перегружен; 504 — апстрим не ответил вовремя (таймаут). Путают их постоянно при отладке балансировщиков и API-gateway.
088. 401 vs 403, 400 vs 422, 201 vs 204
middle
Короткий ответ: 401 — «кто ты?» (нет аутентификации), 403 — «я знаю кто ты, но нельзя» (нет авторизации). 400 — невалидный синтаксис, 422 — валидный синтаксис, но нарушены бизнес-правила. 201 — создан ресурс (с телом/Location), 204 — успех без тела.
Подробно:
401 vs 403:
- 401 Unauthorized (на деле «не аутентифицирован») — токен отсутствует, истёк или неверен. Сервер обязан вернуть заголовок
WWW-Authenticate. - 403 Forbidden — личность установлена, но прав на действие нет (обычный юзер пытается удалить чужой ресурс). Повтор с тем же токеном не поможет.
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="api"
400 vs 422:
- 400 Bad Request — сервер не смог распарсить запрос: битый JSON, отсутствует обязательный заголовок, неверный тип.
- 422 Unprocessable Entity — JSON валиден и распарсен, но данные не проходят бизнес-валидацию: email уже занят, дата в прошлом, отрицательная сумма.
201 vs 204:
- 201 Created — создан новый ресурс; в теле обычно само представление, в заголовке
Location— его URI. - 204 No Content — операция успешна, но возвращать нечего (типично для DELETE, иногда для PUT/PATCH).
⚠️ Ловушка: многие фреймворки по умолчанию шлют 400 на ошибки валидации. Чёткое разделение 400/422 не обязательно (422 не из core HTTP, а из WebDAV), но полезно: позволяет клиенту различить «я неправильно сформировал запрос» от «данные не прошли проверку». Главное — быть консистентным во всём API.
099. HTTP-заголовки: основные и кастомные
middle
Короткий ответ: Заголовки — метаданные запроса/ответа. Ключевые: Content-Type, Accept, Authorization, Cache-Control, ETag, Cookie, User-Agent. Кастомные раньше писали с префиксом X-, теперь рекомендуется без него.
Подробно:
| Заголовок | Направление | Назначение |
|---|---|---|
Content-Type |
оба | формат тела: application/json, text/html, multipart/form-data |
Accept |
запрос | какие форматы клиент готов принять (content negotiation) |
Authorization |
запрос | креды: Bearer <token>, Basic <base64> |
Cache-Control |
оба | политика кэширования: no-cache, max-age=3600, private |
ETag |
ответ | версия-«отпечаток» ресурса для условных запросов |
Cookie / Set-Cookie |
запрос / ответ | передача и установка cookie |
User-Agent |
запрос | информация о клиенте (браузер/SDK) |
Location |
ответ | URI созданного ресурса или цель редиректа |
Content-Length |
оба | размер тела в байтах |
Accept-Encoding / Content-Encoding |
запрос / ответ | сжатие (gzip, br) |
Кастомные заголовки (например X-Request-Id, Idempotency-Key): RFC 6648 рекомендует не использовать префикс X- для новых заголовков, но на практике он жив (X-Forwarded-For, X-Real-IP).
⚠️ Ловушка: имена заголовков регистронезависимы (Content-Type == content-type), но значения — нет. В HTTP/2 и HTTP/3 имена передаются в нижнем регистре. Не храните секреты в логируемых заголовках без маскирования (Authorization часто попадает в логи прокси).
1010. HTTP/1.0 vs 1.1 vs 2 vs 3
middle
Короткий ответ: 1.0 — соединение на каждый запрос; 1.1 — keep-alive и pipelining; 2 — бинарный протокол с мультиплексированием по одному TCP; 3 — поверх QUIC/UDP без head-of-line blocking на транспорте.
Подробно:
HTTP/1.0 — на каждый запрос новое TCP-соединение (дорого: handshake каждый раз).
HTTP/1.1:
Connection: keep-aliveпо умолчанию — переиспользование соединения.- Pipelining (несколько запросов без ожидания ответов) — но почти не используется из-за head-of-line blocking: медленный первый ответ блокирует остальные.
- Chunked transfer encoding, виртуальные хосты (обязательный
Host). - Браузеры открывают ~6 параллельных соединений на домен (отсюда «domain sharding»).
HTTP/2:
- Бинарный формат вместо текстового.
- Мультиплексирование: много параллельных стримов в одном TCP-соединении, без HOL-блокировки на уровне HTTP.
- Сжатие заголовков (HPACK).
- Server Push (сервер шлёт ресурсы заранее) — на практике редко используется и удаляется из браузеров.
- Остаётся HOL-блокировка на уровне TCP: потеря одного пакета тормозит все стримы.
HTTP/3:
- Поверх QUIC (транспорт на базе UDP).
- Решает TCP HOL-блокировку: стримы независимы на транспортном уровне.
- TLS 1.3 встроен, 0-RTT/1-RTT handshake — быстрее установка соединения.
- Лучше при потерях пакетов и смене сети (мобильные клиенты, connection migration).
⚠️ Ловушка: HTTP/2 не убирает HOL-блокировку полностью — она остаётся на уровне TCP. Именно поэтому появился HTTP/3 на QUIC/UDP. И Server Push в HTTP/2 — это не «панацея»; Chrome выпилил его поддержку.
1111. HTTPS/TLS: зачем и как кратко устроен handshake
middle
Короткий ответ: HTTPS — это HTTP поверх TLS. TLS даёт шифрование (конфиденциальность), целостность и аутентификацию сервера через сертификат. Handshake согласует ключи и проверяет сертификат.
Подробно:
Зачем: без TLS трафик идёт открытым текстом — провайдер/Wi-Fi/прокси могут читать и подменять (MITM). TLS обеспечивает три гарантии: шифрование, целостность (MAC/AEAD), аутентификацию сервера.
Упрощённый TLS 1.2 handshake:
- ClientHello — клиент шлёт версии TLS, набор cipher suites, random.
- ServerHello — сервер выбирает cipher, шлёт сертификат (цепочку до доверенного CA).
- Клиент проверяет сертификат (подпись CA, срок, домен в CN/SAN).
- Обмен ключами (ECDHE) → общий симметричный сеансовый ключ.
- Дальше данные шифруются симметрично (быстро).
TLS 1.3 сократил handshake до 1-RTT (и 0-RTT для повторных подключений), убрал устаревшие шифры.
⚠️ Ловушка: TLS аутентифицирует сервер (через сертификат), но по умолчанию не клиента. Для двусторонней аутентификации нужен mTLS (клиентские сертификаты). Также: «зелёный замочек» означает лишь, что соединение шифровано и сертификат валиден, но не что сайт безопасен — фишинг тоже бывает по HTTPS. (Детали сертификатов/CA — в файле по сетям.)
1212. Почему HTTP stateless и как держат состояние?
concept
Короткий ответ: Каждый HTTP-запрос самодостаточен — сервер не обязан помнить предыдущие. Это упрощает масштабирование (любой запрос на любой инстанс). Состояние «прикрепляют» через cookie+сессию или токены.
Подробно:
Stateless значит: вся информация для обработки запроса — в самом запросе. Сервер не хранит контекст между запросами. Плюсы: горизонтальное масштабирование, отказоустойчивость, простое кэширование, любой инстанс за балансировщиком обработает запрос.
Но приложениям нужно состояние (кто залогинен, корзина). Способы:
- Session-based (server-side state): сервер хранит сессию, клиенту отдаёт
Set-Cookie: session_id=.... На каждом запросе сервер по id достаёт данные из хранилища (Redis/БД). Минус — требует общего хранилища или sticky sessions. - Token-based (stateless auth): JWT с подписью содержит claims (user id, роли). Сервер проверяет подпись, ничего не хранит. Плюс — масштабируемо; минус — сложно отозвать до истечения.
- Cookie — общий механизм транспорта (хранит и session id, и иногда сам токен).
HTTP/1.1 200 OK
Set-Cookie: session_id=abc123; HttpOnly; Secure; SameSite=Lax
⚠️ Ловушка: хранить сессии в локальной памяти инстанса ломает stateless-преимущество — при балансировке на другой инстанс пользователь «разлогинится». Нужно либо общее хранилище (Redis), либо sticky sessions (что снижает отказоустойчивость), либо stateless-токены.
1313. Что такое REST и его принципы
junior
Короткий ответ: REST (Representational State Transfer) — архитектурный стиль для распределённых систем (диссертация Роя Филдинга). Основан на ресурсах, единообразном интерфейсе и stateless-взаимодействии поверх HTTP.
Подробно:
Шесть ограничений REST:
- Client-Server — разделение ответственности UI и хранения данных.
- Stateless — сервер не хранит клиентский контекст между запросами.
- Cacheable — ответы должны помечаться как кэшируемые/нет.
- Uniform Interface — единообразие: идентификация ресурсов через URI, манипуляция через представления, самоописательные сообщения, HATEOAS.
- Layered System — клиент не знает, общается ли он напрямую с сервером или через прокси/балансировщик.
- Code on Demand (опционально) — сервер может прислать исполняемый код (например JS).
Ключевая идея: ресурсы (существительные) идентифицируются URI, а HTTP-методы (глаголы) задают операции над ними.
⚠️ Ловушка: большинство «REST API» на практике — это REST уровня 2 по Ричардсону (ресурсы + методы + коды), без HATEOAS. Строго говоря, без HATEOAS API не «по-настоящему RESTful» (по Филдингу), но индустрия называет такие API REST. На собесе уточните этот нюанс.
1414. Уровни зрелости Ричардсона
middle
Короткий ответ: Модель Ричардсона описывает 4 уровня «RESTfulness»: 0 — RPC поверх HTTP, 1 — ресурсы, 2 — HTTP-глаголы и коды, 3 — HATEOAS.
Подробно:
- Уровень 0 (The Swamp of POX) — один endpoint, всё через POST, HTTP как транспорт для RPC. Пример:
POST /apiс телом, описывающим метод. - Уровень 1 — Ресурсы. Появляются отдельные URI на сущности:
/users/1,/orders/5. Но методы ещё не используются по семантике (всё через POST/GET). - Уровень 2 — HTTP-глаголы и коды. Правильное использование GET/POST/PUT/DELETE и кодов статусов (200/201/404...). Большинство реальных API здесь.
- Уровень 3 — HATEOAS. Ответы содержат гиперссылки на доступные действия; клиент «навигирует» по API, не зашивая URI.
{
"id": 5,
"status": "pending",
"_links": {
"self": { "href": "/orders/5" },
"cancel": { "href": "/orders/5/cancel", "method": "POST" }
}
}
⚠️ Ловушка: уровни — это эвристика, а не строгая лестница. Уровень 3 (HATEOAS) на практике редок: усложняет клиент и сервер, выгода спорна для machine-to-machine API с фиксированным контрактом. Не путайте «RESTful» в маркетинге с реальным уровнем 3.
1515. Что значит RESTful: дизайн URL ресурсов
junior
Короткий ответ: URL должны идентифицировать ресурсы (существительные во множественном числе), а не действия. Глаголы выражаются HTTP-методами. Вложенность отражает иерархию ресурсов.
Подробно:
Хорошо:
GET /users # список пользователей
POST /users # создать
GET /users/42 # один пользователь
PUT /users/42 # заменить
PATCH /users/42 # частично обновить
DELETE /users/42 # удалить
GET /users/42/orders # заказы пользователя 42 (вложенность)
GET /users/42/orders/7 # конкретный заказ
Правила:
- Существительные во множественном числе:
/users, не/userи не/getUser. - Без глаголов в URI: не
/createUser, не/users/42/delete. - Вложенность для отношений «принадлежит»:
/users/42/orders. Но не углубляйтесь дальше 2 уровней — лучше/orders/7, чем/users/42/orders/7/items/3/.... - kebab-case или lowercase:
/order-items, не/orderItems. - Действия, не ложащиеся на CRUD, оформляют как «контроллер-ресурс»:
POST /orders/7/cancel,POST /payments/9/refund— допустимый компромисс.
⚠️ Ловушка: не пихать действия в query-параметры (GET /users?action=delete&id=42) — это RPC уровня 0 и нарушение safe-семантики GET. И не плодить глубокую вложенность: глубокие URI хрупкие и неудобны. Фильтры — в query (?status=active), не в path.
1616. HATEOAS
senior
Короткий ответ: HATEOAS (Hypermedia As The Engine Of Application State) — ответы содержат гиперссылки на возможные следующие действия, чтобы клиент не хардкодил URI и переходы.
Подробно:
Идея: клиент знает только entry point, а дальше «следует ссылкам», как человек по сайту. Сервер в каждом ответе сообщает, что можно сделать с ресурсом в его текущем состоянии.
{
"order_id": 7,
"status": "paid",
"total": 5000,
"_links": {
"self": { "href": "/orders/7" },
"invoice": { "href": "/orders/7/invoice" },
"refund": { "href": "/orders/7/refund", "method": "POST" }
}
}
Если заказ ещё не оплачен — ссылки refund не будет, зато появится pay. Клиент реагирует на наличие ссылки, а не зашивает бизнес-логику.
Форматы: HAL (_links), JSON:API (links), Siren.
⚠️ Ловушка: HATEOAS звучит красиво, но в реальности почти не внедряют: клиенты всё равно знают структуру API, а накладные расходы (генерация ссылок, разбор гипермедиа) высоки. На собесе важно знать, что это и почему уровень 3 — редкость, а не считать его обязательным.
1717. Версионирование API: URL vs header
middle
Короткий ответ: Версию API задают через URL (/v1/users), заголовок (Accept: application/vnd.api+json; version=1) или query-параметр. URL-версионирование самое популярное и наглядное.
Подробно:
1. В пути URL (самый распространённый):
GET /api/v1/users
Плюсы: видно сразу, легко роутить и кэшировать, удобно тестировать в браузере. Минусы: «нарушает» идею, что URI идентифицирует один ресурс.
2. В заголовке (media type versioning):
GET /api/users
Accept: application/vnd.example.v2+json
Плюсы: URI чистый и стабильный. Минусы: менее наглядно, сложнее тестировать вручную, легко забыть.
3. Кастомный заголовок:
GET /api/users
X-API-Version: 2
4. Query-параметр: GET /api/users?version=2 — просто, но засоряет query.
⚠️ Ловушка: версионируйте только при breaking changes. Добавление нового опционального поля или нового endpoint — обратно совместимо и не требует новой версии. Не плодите v2, v3 на каждое изменение — поддержка старых версий дорогая. Семантика: ломающие изменения = новая major-версия.
1818. Пагинация: offset vs cursor
middle
Короткий ответ: Offset-пагинация (?limit=20&offset=40) проста, но медленна на больших offset и неустойчива к вставкам. Cursor-пагинация (?limit=20&cursor=...) стабильна и быстра, но не даёт прыгать на произвольную страницу.
Подробно:
Offset / page-based:
GET /users?limit=20&offset=40 # или ?page=3&per_page=20
- Просто, поддерживает «перейти на страницу N».
- Минусы:
OFFSET 100000в SQL медленный (БД сканирует и отбрасывает строки). При вставке/удалении между запросами страницы «съезжают» — можно увидеть дубль или пропустить запись.
Cursor / keyset:
GET /users?limit=20&cursor=eyJpZCI6MTIzfQ==
{
"data": [ ... ],
"next_cursor": "eyJpZCI6MTQzfQ==",
"has_more": true
}
- Cursor кодирует позицию (например
idпоследнего элемента). SQL:WHERE id > :last_id ORDER BY id LIMIT 20— использует индекс, быстро на любой глубине. - Стабильна при вставках. Минусы: нельзя прыгнуть на произвольную страницу, нужна стабильная сортировка по уникальному полю.
⚠️ Ловушка: offset-пагинация без стабильной сортировки (ORDER BY created_at при равных timestamp) выдаёт недетерминированный порядок и дубли/пропуски на границах страниц. Сортируйте по уникальному ключу (или составному с id). Для бесконечной ленты (feed) cursor почти всегда правильный выбор.
1919. Фильтрация, сортировка, поиск
middle
Короткий ответ: Фильтры, сортировку и поиск передают через query-параметры коллекции, не плодя endpoint'ы.
Подробно:
GET /products?category=books&price_min=100&price_max=500&sort=-price,name&q=rest&fields=id,title,price
- Фильтрация:
?status=active&category=books. Для диапазонов —price_min/price_maxили синтаксисprice[gte]=100. - Сортировка:
?sort=-price,name(минус = по убыванию). Несколько полей через запятую. - Поиск: полнотекстовый — отдельный параметр
?q=.... - Sparse fieldsets:
?fields=id,title— вернуть только нужные поля (борьба с over-fetching).
⚠️ Ловушка: принимать произвольные поля фильтра/сортировки напрямую в SQL — риск SQL-инъекции и нагрузки на неиндексированные поля. Валидируйте имена полей по белому списку и ограничивайте, по чему можно сортировать/фильтровать (только индексированные колонки).
2020. Единый формат обработки ошибок
middle
Короткий ответ: Все ошибки API должны иметь единый машиночитаемый формат тела с кодом, сообщением и деталями, плюс корректный HTTP-статус. Стандарт — RFC 7807/9457 (Problem Details).
Подробно:
Формат Problem Details (Content-Type: application/problem+json):
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/problem+json
{
"type": "https://api.example.com/errors/validation",
"title": "Validation failed",
"status": 422,
"detail": "Поле email уже используется",
"instance": "/users",
"errors": [
{ "field": "email", "code": "duplicate", "message": "email уже занят" }
]
}
Принципы:
- Стабильный машиночитаемый
code/type(клиент ветвится по нему, не по тексту). - Человекочитаемое
message/detail. - Детали по полям для валидации.
- Не светить стектрейсы и внутренние детали в проде.
trace_id/request_idдля корреляции с логами.
⚠️ Ловушка: возвращать ошибки в разном формате на разных endpoint'ах (где-то строка, где-то объект, где-то 200 с success:false). Клиент не сможет обрабатывать ошибки единообразно. Зафиксируйте формат на уровне middleware/exception handler.
2121. Rate limiting и 429
middle
Короткий ответ: Rate limiting ограничивает число запросов за период; при превышении возвращается 429 Too Many Requests с заголовком Retry-After и/или RateLimit-*.
Подробно:
HTTP/1.1 429 Too Many Requests
Retry-After: 30
RateLimit-Limit: 100
RateLimit-Remaining: 0
RateLimit-Reset: 30
Алгоритмы: Token Bucket (плавные всплески), Leaky Bucket, Fixed Window, Sliding Window Log/Counter. Token bucket и sliding window — самые распространённые.
Лимитируют по ключу: API-ключ, user id, IP. Хранят счётчики обычно в Redis (атомарные инкременты с TTL).
Зачем: защита от DDoS/abuse, справедливое распределение ресурсов, защита бэкенда от перегрузки, тарификация (free vs paid tier).
⚠️ Ловушка: rate limit по IP ломается за NAT/прокси (много юзеров за одним IP) и обходится сменой IP. Лучше лимитировать по аутентифицированному субъекту. Также важен Retry-After, чтобы клиент знал, когда повторить, и реализовывал exponential backoff, а не бил в стену.
2222. Идемпотентность, повторы и at-least-once
middle
Короткий ответ: В распределённых системах доставка часто at-least-once (сообщение может прийти не раз). Чтобы повторы не дублировали эффект, обработчики делают идемпотентными (dedup по ключу/ID).
Подробно:
Гарантии доставки:
- at-most-once — не более раза, но возможна потеря.
- at-least-once — не менее раза, возможны дубли. Практичный дефолт (ретраи при таймаутах, переотправка очередей).
- exactly-once — идеал, недостижим строго; эмулируется через at-least-once + идемпотентность/дедупликацию.
Стратегия: at-least-once на транспорте + идемпотентный приёмник = эффект «ровно один раз». Дедуплицируют по message_id/Idempotency-Key (таблица обработанных ID, уникальный индекс).
Ретраи на клиенте: повторять можно идемпотентные методы (GET, PUT, DELETE), для POST — только с idempotency key. Использовать exponential backoff + jitter, уважать Retry-After.
⚠️ Ловушка: ретраить POST без idempotency key опасно — таймаут не значит «не выполнилось». Запрос мог дойти и обработаться, а ответ потеряться; повтор создаст дубликат (двойное списание). Всегда различайте «не дошёл» и «дошёл, но ответ потерян».
2323. REST vs RPC vs GraphQL vs gRPC
middle
Короткий ответ: REST — ресурсы поверх HTTP-глаголов; RPC — вызов удалённых процедур (действия); GraphQL — один endpoint с гибким языком запросов; gRPC — бинарный RPC на protobuf поверх HTTP/2.
Подробно:
| Критерий | REST | RPC (JSON-RPC) | GraphQL | gRPC |
|---|---|---|---|---|
| Модель | ресурсы/существительные | процедуры/действия | граф данных | процедуры/действия |
| Транспорт | HTTP | HTTP | HTTP (обычно POST) | HTTP/2 |
| Формат | JSON | JSON | JSON | protobuf (бинарь) |
| Endpoint'ы | много URI | один (метод в теле) | один (/graphql) |
сервисы/методы |
| Over/under-fetch | возможен | возможен | решён (клиент задаёт поля) | фикс. контракт |
| Схема/типы | OpenAPI (опц.) | слабо | строгая (SDL) | строгая (.proto) |
| Стриминг | SSE/WS отдельно | нет | subscriptions | встроенный |
| Кэш по HTTP | отличный | слабый | слабый (POST) | нет |
| Браузер | нативно | да | да | нужен grpc-web |
Когда что:
- REST — публичные API, CRUD, нужно HTTP-кэширование и простота.
- RPC — внутренние сервисы с операциями, не ложащимися на CRUD.
- GraphQL — клиенты с разными потребностями в данных (мобайл + веб), агрегация многих источников, борьба с over/under-fetching.
- gRPC — высоконагруженное межсервисное общение (microservices), нужен стриминг и низкая латентность.
⚠️ Ловушка: GraphQL не «лучше REST» по умолчанию. Он переносит сложность на сервер: HTTP-кэширование почти не работает (всё POST на один URL), легко словить N+1-запросы и ресурсоёмкие вложенные запросы (нужны DataLoader, ограничение глубины/сложности). Для простого CRUD REST проще и эффективнее.
2424. Over-fetching и under-fetching: как GraphQL решает
middle
Короткий ответ: Over-fetching — сервер возвращает больше данных, чем нужно клиенту. Under-fetching — данных не хватает, нужно несколько запросов. GraphQL позволяет клиенту запросить ровно нужные поля за один запрос.
Подробно:
В REST: GET /users/42 вернёт весь объект пользователя, даже если нужно только имя — over-fetching. А чтобы показать пользователя с его заказами и адресами, нужно GET /users/42, GET /users/42/orders, GET /users/42/addresses — under-fetching (N+1 round-trips).
GraphQL — клиент описывает, что хочет, и получает это за один запрос:
query {
user(id: 42) {
name
orders(last: 3) { id total }
}
}
{ "data": { "user": { "name": "Анна", "orders": [ ... ] } } }
Частичные решения в REST: sparse fieldsets (?fields=name), embed/expand (?include=orders), композитные endpoint'ы (BFF — Backend for Frontend).
⚠️ Ловушка: GraphQL лечит over/under-fetching на клиенте, но на сервере возникает N+1 проблема (резолвер каждого поля бьёт в БД). Решается DataLoader (батчинг + кэш в рамках запроса). Без этого GraphQL может быть медленнее REST.
2525. gRPC: protobuf, HTTP/2, стриминг
middle
Короткий ответ: gRPC — RPC-фреймворк от Google: контракт в .proto, сериализация в бинарный protobuf, транспорт HTTP/2, поддержка четырёх видов стриминга. Быстрый и типобезопасный, идеален для межсервисного общения.
Подробно:
Контракт описывается в .proto, из него генерируется клиент и сервер на многих языках:
service UserService {
rpc GetUser (GetUserRequest) returns (User);
rpc ListUsers (ListRequest) returns (stream User); // server streaming
}
message GetUserRequest { int32 id = 1; }
message User { int32 id = 1; string name = 2; }
Виды вызовов:
- Unary — запрос → ответ (как обычный RPC).
- Server streaming — один запрос → поток ответов.
- Client streaming — поток запросов → один ответ.
- Bidirectional streaming — двунаправленный поток поверх одного HTTP/2-соединения.
Преимущества: компактный бинарный формат (меньше REST/JSON), мультиплексирование HTTP/2, строгая схема и кодогенерация, низкая латентность. Минусы: бинарь не читается человеком, в браузере напрямую не работает (нужен grpc-web + прокси), слабее HTTP-кэширование.
⚠️ Ловушка: gRPC требует HTTP/2 end-to-end; не каждый прокси/балансировщик/legacy-инфраструктура это поддерживает. И отлаживать бинарный protobuf руками (как curl с JSON) нельзя — нужен grpcurl. Для публичных браузерных API gRPC обычно неудобен.
2626. WebSockets vs polling vs SSE
middle
Короткий ответ: Polling — клиент периодически опрашивает сервер; long polling — держит запрос открытым до события; SSE — однонаправленный поток событий сервер→клиент; WebSocket — полнодуплексный двусторонний канал.
Подробно:
| Технология | Направление | Транспорт | Когда |
|---|---|---|---|
| Short polling | клиент тянет периодически | обычный HTTP | простота, нечастые обновления |
| Long polling | сервер отвечает при событии | HTTP (держит запрос) | realtime без WS, fallback |
| SSE | сервер → клиент (one-way) | HTTP, text/event-stream |
ленты, нотификации, прогресс |
| WebSocket | дуплекс (оба направления) | апгрейд HTTP → ws | чат, игры, коллаборация |
Short polling — setInterval(fetch, 5000). Просто, но лишний трафик и задержка.
Long polling — запрос висит, сервер отвечает при появлении данных, клиент сразу переоткрывает. Меньше пустых ответов, но держит соединения.
SSE — сервер шлёт события в одном долгом HTTP-ответе:
GET /events HTTP/1.1
Accept: text/event-stream
data: {"type":"new_message","id":5}
data: {"type":"typing"}
Однонаправленно, авто-reconnect, поверх обычного HTTP, работает с HTTP/2. Не подходит для отправки от клиента.
WebSocket — после рукопожатия (Upgrade: websocket, ответ 101 Switching Protocols) — постоянный двунаправленный канал, низкие накладные расходы на сообщение.
⚠️ Ловушка: WebSocket — не всегда правильный выбор. Если данные текут только сервер→клиент (биржевые котировки, уведомления), SSE проще: работает поверх HTTP, авто-reconnect из коробки, проще проксируется и масштабируется. WebSocket сложнее в балансировке (stateful соединения) и не кэшируется.
2727. CORS и preflight
middle
Короткий ответ: CORS (Cross-Origin Resource Sharing) — механизм браузера, разрешающий или запрещающий запросы с одного origin к другому. Для «непростых» запросов браузер сначала шлёт preflight OPTIONS.
Подробно:
Из-за Same-Origin Policy браузер по умолчанию блокирует JS-запросы на другой origin (схема+хост+порт). CORS позволяет серверу явно разрешить кросс-доменные запросы заголовками.
Простые запросы (GET/POST/HEAD с «безопасными» заголовками) идут сразу, сервер отвечает:
Access-Control-Allow-Origin: https://app.example.com
Preflight — для «непростых» запросов (методы PUT/DELETE/PATCH, кастомные заголовки, Content-Type: application/json) браузер сначала шлёт OPTIONS:
OPTIONS /api/users HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: authorization
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: authorization, content-type
Access-Control-Max-Age: 86400
Если ответ разрешает — браузер шлёт настоящий запрос.
⚠️ Ловушка: CORS — защита браузера, а не сервера. curl/Postman/мобильные клиенты игнорируют CORS. Это не замена аутентификации/авторизации. И при Access-Control-Allow-Credentials: true нельзя использовать Allow-Origin: * — нужен конкретный origin (детали в файле по auth).
2828. Content negotiation
middle
Короткий ответ: Content negotiation — механизм, при котором клиент через заголовки Accept* сообщает желаемый формат/язык/кодировку, а сервер выбирает наиболее подходящее представление ресурса.
Подробно:
Клиент указывает предпочтения с весами (q-factor):
GET /report HTTP/1.1
Accept: application/json;q=0.9, application/xml;q=0.5
Accept-Language: ru-RU, en;q=0.7
Accept-Encoding: br, gzip
Сервер выбирает и отвечает:
HTTP/1.1 200 OK
Content-Type: application/json
Content-Language: ru-RU
Content-Encoding: br
Vary: Accept, Accept-Language, Accept-Encoding
Заголовок Vary сообщает кэшам/CDN, что ответ зависит от этих заголовков (нельзя отдать XML-кэш тому, кто просит JSON).
⚠️ Ловушка: забыть Vary при content negotiation — CDN/прокси закэширует один вариант (например gzip-версию) и отдаст её клиенту, не поддерживающему его, либо отдаст JSON тому, кто просил XML. Vary обязателен для корректного кэширования согласованного контента.
3030. HTTP-кэширование: Cache-Control, ETag, 304, CDN
middle
Короткий ответ: HTTP-кэширование управляется заголовками Cache-Control (как долго и где кэшировать) и валидаторами ETag/Last-Modified для условных запросов, возвращающих 304 Not Modified без тела.
Подробно:
Freshness (Cache-Control):
Cache-Control: public, max-age=3600
Cache-Control: private, no-cache
Cache-Control: no-store
max-age=N— сколько секунд ответ «свежий».public— можно кэшировать в общих кэшах (CDN);private— только в браузере.no-cache— кэшировать можно, но перед использованием ревалидировать.no-store— не кэшировать вообще (чувствительные данные).
Validation (условные запросы):
Сервер возвращает ETag (хэш/версия) или Last-Modified. При следующем запросе клиент шлёт:
GET /avatar.png HTTP/1.1
If-None-Match: "v3-abc"
Если ресурс не менялся:
HTTP/1.1 304 Not Modified
ETag: "v3-abc"
304 без тела — экономия трафика; браузер берёт ресурс из кэша.
CDN — географически распределённые кэширующие узлы. Они уважают Cache-Control/ETag/Vary и отдают статику/кэшируемые ответы ближе к пользователю, разгружая origin.
⚠️ Ловушка: no-cache != no-store. no-cache разрешает хранить ответ, но требует ревалидации перед использованием; no-store запрещает хранить вовсе. Для приватных данных (банк, персданные) нужен именно no-store. Частая ошибка — думать, что no-cache означает «не кэшировать».
3131. Сжатие: gzip и brotli
middle
Короткий ответ: Сжатие тела ответа уменьшает трафик. Клиент объявляет поддержку в Accept-Encoding, сервер сжимает и ставит Content-Encoding. gzip — универсальный, brotli (br) — эффективнее для текста.
Подробно:
GET /api/data HTTP/1.1
Accept-Encoding: br, gzip, deflate
HTTP/1.1 200 OK
Content-Encoding: br
Vary: Accept-Encoding
- gzip — повсеместная поддержка, хорошее соотношение скорость/сжатие.
- brotli (br) — лучше сжимает текст (HTML/CSS/JS/JSON), особенно на максимальных уровнях, но медленнее на сжатии; идеален для статики, которую сжимают заранее.
- deflate — устаревший, лучше не использовать.
Сжимать стоит текстовые форматы (JSON, HTML, CSS, JS). Уже сжатые (JPEG, PNG, MP4, zip) сжимать бессмысленно — выгоды нет, только CPU.
⚠️ Ловушка: забыть Vary: Accept-Encoding — CDN может отдать gzip-ответ клиенту, не поддерживающему его. Также: сжатие + шифрование чувствительных данных в одном ответе уязвимо к атакам типа BREACH/CRIME (по размеру сжатого ответа можно угадывать секреты) — не сжимайте ответы, смешивающие секреты и пользовательский ввод.
3232. multipart/form-data и загрузка файлов
middle
Короткий ответ: multipart/form-data — формат тела для отправки бинарных файлов и полей формы вместе. Каждая часть отделена boundary и имеет свои заголовки.
Подробно:
POST /upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----X9Z
------X9Z
Content-Disposition: form-data; name="title"
Мой документ
------X9Z
Content-Disposition: form-data; name="file"; filename="doc.pdf"
Content-Type: application/pdf
<бинарные данные PDF>
------X9Z--
boundaryразделяет части; каждая часть — со своимиContent-Dispositionи опц.Content-Type.- Подходит для смешанных данных (текстовые поля + файлы).
- Альтернатива:
application/x-www-form-urlencoded(только текст, без файлов).
Для больших файлов:
- Chunked / resumable upload (tus, S3 multipart upload) — загрузка частями с возможностью докачки.
- Presigned URLs — клиент грузит напрямую в объектное хранилище (S3), минуя бэкенд.
⚠️ Ловушка: не загружать большие файлы в память сервера целиком — нужен стриминг на диск/в хранилище и лимит размера (413 Payload Too Large). И обязательно валидировать тип/размер на сервере: Content-Type от клиента можно подделать, проверяйте «магические байты» и ограничивайте расширения, чтобы не загрузили исполняемый файл.
3333. Структура URL: scheme/host/path/query/fragment
junior
Короткий ответ: URL состоит из схемы, хоста (+порт), пути, строки запроса и фрагмента. Path-параметры идентифицируют ресурс, query-параметры — фильтруют/настраивают.
Подробно:
https://api.example.com:443/v1/users/42?fields=name&active=true#section
└─┬─┘ └──────┬───────┘└┬┘└────┬─────┘└────────┬────────────┘└──┬──┘
scheme host port path query fragment
- scheme — протокол (
https,http,ws). - host — домен или IP; port — опционален (443 для https, 80 для http).
- path — иерархический путь к ресурсу.
- query — пары
key=valueчерез&после?. - fragment (
#...) — якорь, на сервер не отправляется (обрабатывается браузером).
Path vs query params:
- Path — идентификация конкретного ресурса (обязательная, иерархическая):
/users/42. - Query — необязательные модификаторы коллекции: фильтр, сортировка, пагинация:
?status=active&limit=20.
⚠️ Ловушка: fragment (#...) никогда не доходит до сервера — нельзя класть туда данные для API. Также не кладите секреты (токены) в query — они попадают в логи серверов, прокси, историю браузера, заголовок Referer. Токены — в заголовок Authorization.
3434. Webhooks: зачем и как обезопасить
middle
Короткий ответ: Webhook — HTTP-callback: вместо опроса вашего API сторонним сервисом, сервис сам шлёт POST на ваш URL при событии. Это «push» вместо «pull». Безопасность — через подпись тела.
Подробно:
Пример: платёжный провайдер при успешной оплате шлёт вам POST:
POST /webhooks/payments HTTP/1.1
Content-Type: application/json
X-Signature: t=1700000000,v1=5257a869e7...
{"event":"payment.succeeded","payment_id":"p_123","amount":5000}
Безопасность и надёжность:
- Подпись (HMAC): провайдер подписывает тело секретом, вы пересчитываете HMAC и сравниваете — это подтверждает подлинность и целостность.
- Защита от replay: проверяйте timestamp в подписи и отвергайте старые запросы.
- HTTPS обязателен.
- Идемпотентность: webhook'и доставляются at-least-once — обрабатывайте по
event_idс дедупликацией (один и тот же event может прийти дважды). - Быстрый 2xx: отвечайте быстро (200), тяжёлую обработку выносите в очередь; иначе провайдер сочтёт доставку неудачной и будет ретраить.
⚠️ Ловушка: доверять телу webhook без проверки подписи — любой может подделать payment.succeeded и получить товар бесплатно. Сравнение подписей делайте через constant-time comparison (защита от timing-атак), а не обычным ==. И не забывайте про дедупликацию — повторная доставка не должна выдавать товар дважды.
3535. Зачем идемпотентность и где она спасает?
concept
Короткий ответ: Идемпотентность позволяет безопасно повторять запросы в ненадёжной сети, не создавая дубликатов и побочных эффектов. Спасает при таймаутах, ретраях, повторной доставке сообщений.
Подробно:
Сеть ненадёжна: запрос может дойти, а ответ — потеряться. Клиент видит таймаут и не знает, выполнилась операция или нет. Варианты:
- Не повторять — рискуем потерять операцию.
- Повторять идемпотентную операцию — безопасно, эффект тот же.
- Повторять неидемпотентную (POST оплаты) без защиты — дубликат (двойное списание).
Где спасает:
- Платежи: idempotency key не даёт списать дважды при ретрае.
- Очереди сообщений: at-least-once доставка → дубли; идемпотентный consumer (dedup по message_id) делает эффект «exactly-once».
- Распределённые транзакции / Saga: шаги повторяемы при сбоях.
- Инфраструктура (Terraform, k8s): desired state применяется идемпотентно — повторный apply ничего не ломает.
⚠️ Ловушка: идемпотентность нужно проектировать заранее — она почти не «прикручивается» поверх готовой неидемпотентной системы. Если архитектура изначально допускает at-least-once (а в распределённых системах это норма), обработчики обязаны быть идемпотентными с первого дня.
3636. Чем REST лучше или хуже GraphQL?
concept
Короткий ответ: REST проще, отлично кэшируется по HTTP и предсказуем; GraphQL гибче для клиента (нет over/under-fetching) и хорош при множестве разнородных клиентов, но переносит сложность на сервер и ломает HTTP-кэширование.
Подробно:
REST лучше когда:
- Простой CRUD, ресурсная модель ложится естественно.
- Нужно HTTP-кэширование (CDN, ETag, 304) — работает «из коробки».
- Публичное API с понятным контрактом, важна простота интеграции.
- Файлы, стриминг, разные content-types.
GraphQL лучше когда:
- Много клиентов с разными потребностями в данных (мобайл экономит трафик, веб тянет больше).
- Агрегация данных из многих сервисов в один запрос (BFF).
- Часто меняющиеся требования к выборке полей без правки бэкенда.
- Сильная типизация и интроспекция схемы важны.
Цена GraphQL:
- HTTP-кэширование почти не работает (всё POST на
/graphql). - N+1-проблема (нужен DataLoader).
- Сложно ограничивать дорогие/глубокие запросы (нужны depth/complexity limits, persisted queries).
- Rate limiting сложнее (один запрос != одна «стоимость»).
⚠️ Ловушка: выбор «REST vs GraphQL» — не про «что современнее», а про контекст. На собесе плохой ответ — «GraphQL лучше, потому что новее». Хороший — «зависит от: числа и разнообразия клиентов, важности кэширования, сложности агрегации». Часто их даже комбинируют: GraphQL для клиентского BFF, REST/gRPC между сервисами.
Источники
Источники и редакционная политика
Материалы RecallDeck сопоставлены с официальной документацией и открытыми публикациями компаний, когда первичный источник доступен. Мы не связаны с упомянутыми работодателями, не публикуем конфиденциальные задания и не продаём места в подборках. Формат найма может меняться — уточняйте его у рекрутера.
От чтения к воспроизведению
Отрепетируйте полный цикл интервью.
RecallDeck возвращает сложные темы по расписанию и помогает удерживать в памяти язык, SQL, архитектуру и поведенческие истории.