Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Питання на співбесіді з API

Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.

100 питань

Стайлгайд API («ресурси в множині», «поля в snake_case», «кожна помилка має опис») у вікі мало хто читає. Якщо API описано специфікацією OpenAPI, правила можна перевіряти автоматично, як код лінтером.

Spectral - лінтер для OpenAPI (і AsyncAPI) з набором вбудованих правил і власними правилами:

# .spectral.yaml
extends: ["spectral:oas"]

rules:
  operation-description: error

  paths-kebab-case:
    description: Шляхи мають бути в kebab-case
    severity: error
    given: $.paths[*]~
    then:
      function: pattern
      functionOptions:
        match: "^(/[a-z0-9-{}]+)+$"

  properties-snake-case:
    description: Властивості схем - snake_case
    severity: warn
    given: $.components.schemas[*].properties[*]~
    then:
      function: casing
      functionOptions: { type: snake }

  errors-documented:
    description: Кожна операція описує відповідь 4xx
    severity: warn
    given: $.paths[*][*].responses
    then:
      function: schema
      functionOptions:
        schema: { anyOf: [{ required: ["400"] }, { required: ["404"] }, { required: ["422"] }] }
npx @stoplight/spectral-cli lint openapi.json

given - JSONPath до частини документа, then - перевірка (pattern, casing, truthy, schema, власні функції на JavaScript).

Що варто перевіряти:

  • узгодженість: однаковий стиль назв, однакова пагінація й формат помилок у всіх ендпойнтах;
  • повноту: описи операцій, приклади, задокументовані помилки й автентифікація;
  • безпеку: кожна операція має схему безпеки, немає ендпойнтів без автентифікації випадково;
  • заборонені практики: дієслова в шляхах, 200 з тілом помилки, відсутність обмеження розміру сторінки.

У CI разом з лінтером - пошук змін, що ламають клієнтів: порівняння специфікації гілки зі специфікацією основної гілки (інструменти на кшталт oasdiff). Видалене поле, новий обов'язковий параметр, змінений тип - помилка збирання, якщо зміна не позначена як навмисна.

Як впроваджувати:

  • починати з попереджень, а не помилок, - інакше існуюче API не пройде перевірку;
  • нові правила - як error лише для нових ендпойнтів чи після виправлення старих;
  • стайлгайд у вигляді набору правил Spectral - виконуваний, і його можна перевикористати між командами (публікуючи як npm-пакет).

Для Laravel з генерацією специфікації (Scramble): експорт специфікації в CI → лінтинг → порівняння з основною гілкою. Так правила застосовуються до реального API, а не лише до документації.

Докладніше в документації: Spectral: лінтер для OpenAPI

Найнебезпечніші зміни API - ті, що непомітно ламають клієнтів: розробник «трохи покращив» ресурс, тести бекенду зелені, а інтеграція партнера падає. Захист - зробити зміни контракту видимими й перевірюваними.

Що ламає клієнтів (breaking changes):

  • видалення чи перейменування поля, ендпойнта, параметра;
  • зміна типу поля (число → рядок) чи формату (дата без поясу → з поясом);
  • новий обов'язковий параметр запиту чи поле тіла;
  • звуження допустимих значень (нові обмеження валідації, видалене значення enum);
  • зміна кодів статусу й формату помилок;
  • зміна поведінки за замовчуванням (сортування, розмір сторінки).

Що зазвичай безпечно: нові необов'язкові параметри, нові поля у відповіді, нові ендпойнти. «Зазвичай» - бо клієнт зі строгою десеріалізацією чи вичерпною перевіркою enum може зламатися й від нового значення. Це варто прямо прописати в політиці API: «клієнти мають ігнорувати невідомі поля й значення».

Автоматична перевірка в CI:

oasdiff breaking main-openapi.yaml branch-openapi.yaml --fail-on ERR
oasdiff changelog main-openapi.yaml branch-openapi.yaml
  • breaking - список змін, що ламають, з рівнями серйозності; збирання падає, якщо зміна не дозволена;
  • changelog - людиночитний перелік змін, який можна використати в описі pull request і журналі змін.

Специфікацію для порівняння беруть з основної гілки (згенеровану Scramble чи написану вручну) - так навіть «непомітні» зміни ресурсу Laravel стають видимими в рев'ю.

Журнал змін для споживачів:

  • дата й версія кожної зміни, групування: додано, змінено, застаріло, видалено;
  • посилання на документацію й інструкція з міграції для кожної зміни, що ламає;
  • оголошення заздалегідь: дата застарівання і дата видалення; для відповідей - заголовки Deprecation і Sunset;
  • канал повідомлень для інтеграторів (розсилка, RSS, сторінка статусу), а не лише сторінка, яку ніхто не відкриває.

Процес для змін, що ламають: нова версія ендпойнта чи поля, паралельна підтримка старого, моніторинг використання застарілого (логування запитів зі старими полями й версіями, щоб знати, хто ще залежить), повідомлення конкретним клієнтам, лише потім видалення.

Найкращий журнал змін - коротший: більшість змін, що ламають, можна замінити адитивними (нове поле поруч зі старим), і тоді клієнтам взагалі нічого не треба робити.

Докладніше в документації: oasdiff: порівняння OpenAPI-специфікацій

GraphQL дає клієнту змогу самому будувати запит - а отже й побудувати дуже дорогий. Один HTTP-запит може змусити сервер виконати мільйони операцій.

Типові атаки й проблеми:

# глибина: циклічні зв'язки дають експоненційне зростання
{ user(id: 1) { friends { friends { friends { friends { name } } } } } }

# ширина: величезні списки
{ posts(first: 100000) { comments(first: 1000) { author { name } } } }

# псевдоніми: одне поле, викликане тисячу разів в одному запиті
{ a1: login(email: "...", password: "1") a2: login(email: "...", password: "2") ... }

Останній приклад обходить обмеження частоти на рівні HTTP: один запит - тисяча спроб входу.

Захист - кілька рівнів:

  • обмеження глибини запиту (наприклад, 8-10 рівнів);
  • аналіз складності: кожному полю призначається «вартість» (списки - помножена на first), запит понад ліміт відхиляється до виконання;
  • обов'язкова пагінація з максимумом - жодних списків без first чи з first: 100000;
  • обмеження частоти за складністю, а не за кількістю HTTP-запитів: бюджет «очок» на клієнта за хвилину (так працює GitHub GraphQL API);
  • обмеження псевдонімів і пакетних запитів (batching кількох операцій в одному HTTP-запиті);
  • тайм-аути виконання запиту.

Інтроспекція в продакшені. Вона показує всю схему, включно з внутрішніми полями й мутаціями. Для публічних API зі схемою як документацією це нормально; для API лише свого фронтенду - вимкнути.

Збережені (persisted) запити / довірені документи: клієнт надсилає не текст запиту, а його хеш з переліку, відомого серверу під час збирання. Сервер виконує лише заздалегідь відомі запити - довільні атаки неможливі взагалі. Найсильніший захист для API, яке використовує лише ваш фронтенд, плюс бонус - запити можна робити через GET і кешувати на CDN.

Авторизація на рівні полів. Перевірка лише на верхньому запиті недостатня: доступ до order не означає доступу до order.customer.paymentMethods. Кожен резолвер чутливих даних має перевіряти права.

Повідомлення про помилки: у продакшені не віддавати стек і внутрішні деталі в errors - це витік інформації про реалізацію.

У Lighthouse обмеження задаються в config/lighthouse.php (max_query_depth, max_query_complexity, disable_introspection), а вартість полів - директивою @complexity. За замовчуванням ліміти вимкнені - їх треба ввімкнути свідомо.

Докладніше в документації: Безпека GraphQL

Чому REST кешується легко: кожен ресурс має свою адресу, читання - через GET, і вся інфраструктура HTTP (браузер, CDN, проксі) розуміє Cache-Control і ETag без жодної участі застосунку.

Чому з GraphQL складніше:

  • одна адреса /graphql для всього;
  • запити через POST - HTTP-кеші їх не кешують;
  • кожен клієнт формує свій запит - навіть однакові дані запитуються різними наборами полів, і ключ кешу «URL» не працює;
  • одна відповідь змішує дані з різним терміном актуальності (назва товару - години, залишок на складі - секунди).

Рішення на різних рівнях:

1. Нормалізований кеш на клієнті (Apollo Client, urql з Graphcache, Relay). Кеш зберігає об'єкти за __typename + id, а не відповіді цілком. Мутація, що повертає змінений об'єкт, автоматично оновлює його в усіх екранах. Тому корисно мати глобально унікальні ідентифікатори і завжди запитувати id.

2. Збережені запити + GET. Клієнт надсилає хеш заздалегідь відомого запиту і змінні в рядку запиту:

GET /graphql?extensions={"persistedQuery":{"sha256Hash":"ab12..."}}&variables={"id":42}

Тепер відповідь має стабільну адресу, і її можна кешувати на CDN - як REST.

3. Підказки кешування в схемі. Сервер обчислює Cache-Control для відповіді з найкоротшого терміну серед полів (директиви на кшталт @cacheControl(maxAge: 60)), а персональні поля позначає приватними. Відповідь з даними користувача не повинна потрапити в спільний кеш.

4. Кеш на сервері:

  • на рівні резолверів чи DataLoader - кешування окремих сутностей у Redis;
  • кеш цілих відповідей за нормалізованим текстом запиту + змінними + користувачем - простий, але інвалідація складна.

Інвалідація - найважче місце. Зміна одного товару торкається безлічі різних запитів, що його містять. Тому популярний підхід - теги: відповідь позначається тегами сутностей (Product:42), і зміна сутності скидає всі відповіді з цим тегом (так працюють CDN з purge за ключами).

Практичний висновок: якщо важливий кеш на CDN для публічних даних (каталог, статті), - або REST для цих частин, або збережені запити через GET. Для персональних даних основну роботу робить нормалізований кеш клієнта.

Докладніше в документації: Кешування в GraphQL

У Protocol Buffers у повідомленні передаються не назви полів, а їхні номери. Звідси головні правила еволюції схеми: важливо не те, як поле називається, а який у нього номер і тип.

Безпечні зміни:

  • додати нове поле з новим номером. Старі клієнти його проігнорують, нові - отримають значення за замовчуванням, якщо старий сервер його не надіслав;
  • перейменувати поле - номер той самий, на «дроті» нічого не змінилося (але згенерований код зміниться - це зміна API для коду, не для формату);
  • видалити поле, якщо його номер і назву зарезервувати.

Небезпечні зміни:

  • змінити номер поля - для формату це видалення старого поля і поява нового;
  • повторно використати номер видаленого поля з іншим змістом - старі клієнти розберуть нові дані як старе поле, з тихим пошкодженням даних;
  • змінити тип на несумісний (string → int64). Деякі пари сумісні на рівні формату (int32/int64/bool), але з можливим обрізанням значень - покладатися на це не варто;
  • перетворити одиничне поле на repeated чи навпаки - з неочевидними наслідками для різних мов.

Резервування видалених полів:

message Invoice {
  reserved 3, 7;
  reserved "discount", "legacy_status";

  int64 id = 1;
  string number = 2;
  int64 total_cents = 4;
}

Компілятор не дасть використати зарезервовані номери й назви повторно.

Інші практики:

  • значення за замовчуванням у proto3 - 0, "", false неможливо відрізнити від «не задано». Якщо різниця важлива - optional (явна присутність) чи обгорткові типи;
  • перелічення: перше значення має бути 0 і означати «невідомо» (STATUS_UNSPECIFIED = 0), бо саме його отримає старий клієнт для нових значень;
  • версія в назві пакета (billing.v1, billing.v2) - для справді несумісних змін створюється новий пакет і обидва обслуговуються паралельно;
  • автоматична перевірка в CI: інструмент buf breaking порівнює схему з попередньою версією й знаходить ламаючі зміни до злиття.

Порядок розгортання: спершу оновлюються ті, хто читає нове поле (сервери, що його приймають), потім ті, хто починає його надсилати. Видалення - у зворотному порядку: спершу всі припиняють використовувати поле, потім воно резервується.

Докладніше в документації: Protocol Buffers: оновлення типу повідомлення

Подієві API - вебхуки, повідомлення в брокері (Kafka, RabbitMQ), канали WebSocket - мають ті самі проблеми, що й REST: контракт, документація, сумісність. Але є й власні - порядок і дублікати.

Документація - AsyncAPI. Те, чим OpenAPI є для REST, AsyncAPI є для подій: специфікація каналів, повідомлень і їхніх схем:

asyncapi: 3.0.0
info:
  title: Orders events
  version: 1.4.0
channels:
  orderPaid:
    address: orders.paid
    messages:
      orderPaid:
        payload:
          type: object
          required: [id, orderId, paidAt]
          properties:
            id: { type: string }
            orderId: { type: integer }
            paidAt: { type: string, format: date-time }

З неї генеруються документація, типи для споживачів і перевірки повідомлень.

Версіонування подій:

  • додавання полів - безпечне, якщо споживачі ігнорують невідомі поля (це треба вимагати в документації);
  • ламаючі зміни - нова назва чи версія події (order.paid.v2) і паралельна публікація обох версій на перехідний період;
  • версія у вебхуках - часто прив'язується до облікового запису одержувача (як api_version у Stripe): одержувач сам обирає, коли перейти на новий формат.

Порядок подій не гарантований. Повтори, паралельні воркери й мережа змішують порядок: order.shipped може прийти раніше за order.paid. Стратегії для одержувача:

  • мітка часу чи номер версії об'єкта в події - застосовувати лише якщо подія новіша за вже відомий стан;
  • «тонкі» події: подія лише повідомляє «замовлення 42 змінилося», а одержувач запитує актуальний стан через API - порядок перестає мати значення;
  • впорядкування за ключем у брокері (партиції Kafka за order_id) - порядок гарантується в межах однієї сутності, а не глобально.

Дублікати - наслідок доставки «щонайменше один раз»: одержувач зберігає оброблені id подій і пропускає повтори.

Документуйте явно: гарантії доставки (щонайменше один раз), відсутність гарантії порядку, тайм-аути й політику повторів, як перевіряти підпис. Більшість помилок інтеграцій - від неявних припущень одержувача про порядок і унікальність.

Докладніше в документації: AsyncAPI: основні поняття

Коли сторонній сервіс лежить, повтори й тайм-аути не допомагають, а шкодять: кожен запит чекає повний тайм-аут, воркери й процеси PHP зайняті очікуванням, черга росте, а сервіс, що намагається піднятися, отримує лавину запитів.

Circuit Breaker («запобіжник») відстежує помилки й після порогу перестає викликати сервіс на певний час:

Closed (норма)  -- N помилок поспіль -->  Open (виклики не йдуть, одразу помилка)
     ^                                          |
     |                                     минув час
     |                                          v
     +----- успіх ----  Half-open (пропустити пробний запит)  ---- помилка ---> Open

Що це дає:

  • швидка відмова замість 30-секундного очікування: користувач одразу бачить запасний варіант, воркери вільні;
  • сервісу дають відновитися без лавини повторів;
  • запасна поведінка: закешовані дані, відкладена обробка, повідомлення «сервіс тимчасово недоступний».

У Laravel для фонових задач роль запобіжника виконує middleware ThrottlesExceptions:

public function middleware(): array
{
    return [
        (new ThrottlesExceptions(maxAttempts: 10, decaySeconds: 5 * 60))
            ->by('payment-provider')                                  // спільно для всіх джоб провайдера
            ->when(fn (Throwable $e) => $e instanceof ConnectionException
                || ($e instanceof RequestException && $e->response->serverError())),
    ];
}

Після 10 помилок джоби з цим ключем не виконуються 5 хвилин, а повертаються в чергу. З retryUntil() вони не губляться.

Для синхронних викликів простий запобіжник на кеші:

final class CircuitBreaker
{
    public function __construct(private string $service, private int $threshold = 5, private int $cooldown = 60) {}

    public function call(callable $callback, callable $fallback): mixed
    {
        if (Cache::has("circuit:{$this->service}:open")) {
            return $fallback();
        }

        try {
            $result = $callback();
            Cache::forget("circuit:{$this->service}:failures");
            return $result;
        } catch (ConnectionException|RequestException $e) {
            if (Cache::increment("circuit:{$this->service}:failures") >= $this->threshold) {
                Cache::put("circuit:{$this->service}:open", true, $this->cooldown);
            }
            return $fallback();
        }
    }
}

Що важливо:

  • рахувати лише «системні» помилки (мережа, 5xx, тайм-аути), а не 4xx від ваших же некоректних запитів;
  • стан спільний для всіх процесів - у Redis, а не в пам'яті одного воркера;
  • сповіщення: перехід у стан Open - подія для моніторингу й чергового;
  • короткі тайм-аути - запобіжник не замінює їх, а доповнює.

Готові рішення є й у вигляді пакетів, але для більшості Laravel-застосунків ThrottlesExceptions у черзі + короткі тайм-аути + кеш як запасний варіант покривають потреби.

Докладніше в документації: Martin Fowler: Circuit Breaker

У інтеграціях дублікати неминучі: провайдер повторює вебхук, бо не дочекався відповіді; джоба повторюється після тайм-ауту, хоча перша спроба насправді завершилася; користувач двічі натискає «Синхронізувати». Обробка має давати той самий результат незалежно від кількості повторів.

1. Унікальність на вході - журнал оброблених подій:

Schema::create('processed_events', function (Blueprint $table) {
    $table->string('provider');
    $table->string('event_id');
    $table->timestamp('processed_at');
    $table->primary(['provider', 'event_id']);
});
public function handle(): void
{
    DB::transaction(function () {
        $inserted = DB::table('processed_events')->insertOrIgnore([
            'provider' => 'stripe',
            'event_id' => $this->event['id'],
            'processed_at' => now(),
        ]);

        if ($inserted === 0) {
            return;   // уже оброблено
        }

        $this->applyPayment($this->event);
    });
}

Унікальний ключ у базі - єдиний надійний захист від гонитви: перевірка «чи є запис» і вставка окремими запитами пропускають паралельні дублікати.

2. Ідемпотентні операції за природою:

  • updateOrCreate/upsert за зовнішнім ідентифікатором (external_id) замість create;
  • «встановити статус paid» замість «збільшити суму на X»;
  • переходи станів з перевіркою: «оплачене» не можна оплатити вдруге.

3. Унікальні джоби - щоб не ставити в чергу однакову роботу:

#[UniqueFor(600)]
final class SyncCustomer implements ShouldQueue, ShouldBeUnique
{
    public function uniqueId(): string
    {
        return (string) $this->customerId;
    }
}

Поки джоба для цього клієнта в черзі, нові такі ж відкидаються. Потребує кешу з атомарними блокуваннями і спільного кешу для всіх серверів.

4. Вихідні операції з побічними ефектами (створення платежу, відправка SMS) - ключ ідемпотентності в запиті до провайдера, стабільний для повторів тієї самої операції (Idempotency-Key: order-42-capture). Тоді повтор джоби після тайм-ауту не створить другий платіж.

5. Звірка (reconciliation) - останній рубіж: періодичне порівняння стану у вас і в провайдера (оплати за добу, залишки на складі), автоматичне виправлення розбіжностей і звіт про них. Ловить те, що загубилося попри всі механізми.

Типова помилка: покладатися на ShouldBeUnique як на захист від повторної обробки. Він не дає поставити дублікат у чергу, але не захищає від повторного виконання вже взятої джоби після збою воркера - для цього потрібні ідемпотентні операції й унікальні ключі в базі.

Докладніше в документації: Черги: унікальні джоби

Saloon - бібліотека для PHP (з інтеграцією в Laravel), що структурує роботу з API у класи: конектор описує API загалом, запит - один ендпойнт.

use Saloon\Http\Connector;
use Saloon\Traits\Plugins\AcceptsJson;

final class CrmConnector extends Connector
{
    use AcceptsJson;

    public ?int $tries = 3;
    public ?int $retryInterval = 500;
    public ?bool $useExponentialBackoff = true;

    public function __construct(private readonly string $token) {}

    public function resolveBaseUrl(): string
    {
        return 'https://crm.example.com/api/v2';
    }

    protected function defaultAuth(): TokenAuthenticator
    {
        return new TokenAuthenticator($this->token);
    }
}
use Saloon\Enums\Method;
use Saloon\Http\Request;
use Saloon\Http\Response;

final class GetDeal extends Request
{
    protected Method $method = Method::GET;

    public function __construct(private readonly int $id) {}

    public function resolveEndpoint(): string
    {
        return "/deals/{$this->id}";
    }

    public function createDtoFromResponse(Response $response): Deal
    {
        return Deal::fromArray($response->json('data'));
    }
}

$deal = $connector->send(new GetDeal(42))->dto();   // Deal, а не масив

Що дає порівняно з Http:: у класі-клієнті:

  • один запит - один клас: ендпойнт, метод, тіло, заголовки, перетворення відповіді в DTO живуть разом. Великий API (десятки ендпойнтів) не перетворюється на клас-клієнт на тисячу рядків;
  • DTO на виході (createDtoFromResponse + dto()) - решта застосунку не бачить формату провайдера;
  • повтори, автентифікація, пагінація, OAuth2 - готові механізми на рівні конектора;
  • тести: MockClient з відповідями для конкретних класів запитів і фікстури - записані справжні відповіді, що відтворюються в тестах;
  • middleware на рівні конектора - логування, метрики, ідентифікатори кореляції в одному місці.

Як не перестаратися:

  • для двох-трьох викликів Saloon - зайва церемонія; макрос чи невеликий клас-клієнт простіші;
  • DTO мають відображати потреби вашого коду, а не повністю копіювати відповідь провайдера: мапінг лише потрібних полів, перетворення типів (дати, гроші в мінімальних одиницях, енуми статусів) і явна обробка відсутніх полів;
  • не тягнути SDK у предметну область: сервіси застосунку залежать від вашого інтерфейсу (CrmGateway), а Saloon - деталь його реалізації. Тоді зміна провайдера чи бібліотеки не зачіпає бізнес-логіку.

Коли Saloon особливо доречний: ключова інтеграція з великою кількістю ендпойнтів, кілька інтеграцій з однаковими підходами в команді, публікація власного SDK для свого API.

Докладніше в документації: Saloon: конектори

Коли «не приходять SMS» чи «замовлення не потрапили в CRM», перше питання - що саме відбувалося між вашим застосунком і провайдером. Без записів про вихідні запити відповісти неможливо.

Події HTTP-клієнта - точка для централізованого логування без зміни коду інтеграцій:

use Illuminate\Http\Client\Events\ConnectionFailed;
use Illuminate\Http\Client\Events\ResponseReceived;

Event::listen(function (ResponseReceived $event) {
    Log::channel('integrations')->info('http.out', [
        'host' => parse_url($event->request->url(), PHP_URL_HOST),
        'method' => $event->request->method(),
        'path' => parse_url($event->request->url(), PHP_URL_PATH),
        'status' => $event->response->status(),
        'duration_ms' => round(($event->response->transferStats?->getTransferTime() ?? 0) * 1000),
    ]);
});

Event::listen(fn (ConnectionFailed $event) => Log::channel('integrations')->warning('http.out.failed', [
    'url' => $event->request->url(),
]));

Також є RequestSending перед відправкою і глобальні middleware клієнта (Http::globalRequestMiddleware, globalResponseMiddleware) - наприклад, щоб додати заголовок кореляції до всіх вихідних запитів.

Що варто записувати:

  • провайдер, ендпойнт (без параметрів з персональними даними), метод, статус, тривалість, номер спроби;
  • ідентифікатор запиту провайдера з заголовка відповіді (X-Request-Id, CF-Ray) - саме його попросить підтримка провайдера;
  • ваш ідентифікатор кореляції - щоб зв'язати вихідний запит з HTTP-запитом користувача чи джобою, що його породили (Context::add('trace_id', ...) в Laravel додає його в усі журнали);
  • для збоїв - фрагмент тіла відповіді з помилкою.

Чого НЕ записувати: токени й ключі (Authorization), паролі, номери карток, повні тіла з персональними даними. Маскування - в одному місці, у слухачі подій.

Метрики й сповіщення важливіші за журнали:

  • частка помилок і латентність за провайдером (p95);
  • кількість 429 - наближення до лімітів;
  • довжина черги інтеграцій і кількість невдалих джоб;
  • сповіщення, коли частка помилок провайдера перевищує поріг.

Інструменти в Laravel-екосистемі: Pulse (картки повільних вихідних запитів), Telescope (на staging - детально кожен запит), Nightwatch, OpenTelemetry для розподіленого трасування.

Журнал бізнес-операцій інтеграції (таблиця «синхронізації»: що відправлено, коли, результат, зовнішній ID) - окремо від технічних журналів. Його показують підтримці й адміністраторам і використовують для звірки.

Пісочниці провайдерів у staging - щоб збої інтеграцій ловилися до продакшену, а моніторинг працював і там.

Докладніше в документації: HTTP-клієнт: події

Питання з реальних технічних співбесід - 100 питань у 7 темах, розібраних із відповідями. Нижче - розбивка за рівнями та темами, якщо хочете звузити підготовку.

Рівні
Junior 35 Middle 35 Senior 30

Готуєтесь до співбесіди не просто так: зараз на сайті 146 відкритих вакансій Laravel і PHP. Переглянути вакансії