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

Middle: питання на співбесіді з теми «Браузерні API й мережа»

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

5 питань

Браузер застосовує політику одного джерела (same-origin policy): JavaScript зі сторінки https://app.example.com не може читати відповіді від https://api.other.com. Джерело (origin) - це схема + домен + порт: http і https, example.com і api.example.com, порти 3000 і 8000 - різні джерела.

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

CORS (Cross-Origin Resource Sharing) - спосіб, яким сервер дозволяє певним джерелам читати свої відповіді, через заголовки:

Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true

Попередній запит (preflight). Для «непростих» запитів браузер спершу надсилає OPTIONS, щоб запитати дозволу:

  • методи, крім GET, HEAD, POST;
  • власні заголовки (Authorization, X-Requested-With);
  • Content-Type: application/json (простими вважаються лише формові типи й text/plain).
OPTIONS /api/orders
Origin: https://app.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: content-type, authorization

Сервер відповідає, що дозволено (Access-Control-Allow-Methods, -Headers, -Max-Age для кешування дозволу). Лише після цього йде справжній запит.

Що важливо розуміти:

  • CORS виконує браузер, а не сервер. curl, Postman і бекенд-код не мають жодних обмежень. CORS - не захист API від чужих клієнтів;
  • простий запит доходить до сервера і виконується - браузер лише не дає JavaScript прочитати відповідь. Тому CORS не замінює захист від CSRF;
  • з cookies (credentials: 'include') заборонено Access-Control-Allow-Origin: * - потрібне конкретне джерело і Access-Control-Allow-Credentials: true;
  • помилка CORS у консолі часто маскує іншу: сервер повернув 500 чи 404 без CORS-заголовків, і браузер повідомляє про CORS, а не про справжню причину. Варто дивитися запит у вкладці Network.

У Laravel CORS обробляє вбудований middleware HandleCors з налаштуваннями в config/cors.php (публікується php artisan config:publish cors):

'paths' => ['api/*', 'sanctum/csrf-cookie'],
'allowed_origins' => ['https://app.example.com'],
'supports_credentials' => true,

Як обійтися без CORS: розмістити фронтенд і API на одному джерелі (Laravel віддає і сторінки, і /api), або проксувати запити через сервер розробки Vite (server.proxy). Тоді запити same-origin, і CORS не потрібен.

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

History API дає змогу змінювати адресу в рядку браузера і записи в історії переходів без завантаження нової сторінки. На ньому побудовані маршрутизатори SPA (Vue Router, React Router), wire:navigate у Livewire, фільтри каталогів, що відображаються в URL.

pushState - додати новий запис в історію:

history.pushState({ page: 2 }, '', '/jobs?page=2');

Адреса змінилася, кнопка «Назад» поверне на попередню, але запиту на сервер не було і сторінка не перезавантажилася. Що показати - вирішує ваш код.

replaceState - замінити поточний запис, не додаючи нового. Для змін, які не повинні засмічувати історію: введення в поле пошуку, сортування, позиція прокрутки.

popstate - подія, коли користувач переходить по історії кнопками «Назад»/«Вперед»:

window.addEventListener('popstate', (event) => {
  renderPage(event.state?.page ?? 1);   // відновити стан для цього запису
});

Важливо: pushState і replaceState не викликають popstate. Подія спрацьовує лише при навігації по історії. Тому після pushState оновлювати інтерфейс треба самостійно.

Об'єкт стану (перший аргумент) зберігається разом із записом історії і повертається в event.state. Він серіалізується (як structuredClone), має обмеження розміру - туди кладуть невеликі дані (номер сторінки, id), а не весь список товарів.

Що треба зробити серверу: якщо користувач оновить сторінку чи відкриє посилання /jobs?page=2 напряму, запит піде на сервер. Сервер мусить уміти віддати цю сторінку - інакше 404. Для SPA це «fallback» на index.html, у Laravel - звичайний маршрут, що рендерить сторінку з відповідним станом.

Типові вимоги до SPA-навігації, які легко забути:

  • заголовок сторінки (document.title) - змінюється вручну;
  • прокрутка: при переході вперед - на початок, при «Назад» - туди, де користувач був. Браузерне history.scrollRestoration інколи доводиться вимикати й керувати самостійно;
  • фокус і доступність: зчитувачі екрана не знають, що «сторінка змінилася», - фокус переводять на заголовок нового вмісту;
  • аналітика: перегляд сторінки треба відправляти вручну.

Navigation API - новіший інтерфейс із єдиною подією navigate для всіх переходів (посилання, форми, кнопки історії) і перехопленням через event.intercept(). Він простіший для маршрутизаторів, але підтримується ще не всіма браузерами.

Докладніше в документації: History API

Обидві технології дають змогу серверу надсилати дані в браузер без запиту від клієнта. Різниця - у напрямку, протоколі й складності.

Server-Sent Events (SSE) - однобічний потік від сервера до клієнта через звичайне HTTP-з'єднання:

const source = new EventSource('/orders/42/status');

source.addEventListener('status', (event) => {
  const { status } = JSON.parse(event.data);
  updateBadge(status);
});

source.onerror = () => { /* браузер сам перепідключиться */ };

Сервер відповідає з Content-Type: text/event-stream і пише рядки:

event: status
id: 17
data: {"status":"shipped"}

  • автоматичне перепідключення вбудоване; при перепідключенні браузер надсилає Last-Event-ID, і сервер може дослати пропущене;
  • працює через звичайний HTTP: проксі, балансувальники, автентифікація cookies - як у будь-якого запиту;
  • лише текст, лише від сервера;
  • на HTTP/1.1 браузер обмежує ~6 з'єднань на домен - кілька вкладок з SSE можуть їх вичерпати. На HTTP/2 і HTTP/3 обмеження немає.

WebSocket - двобічне постійне з'єднання з власним протоколом:

const socket = new WebSocket('wss://example.com/ws');
socket.onmessage = (event) => render(JSON.parse(event.data));
socket.send(JSON.stringify({ type: 'typing', chat: 7 }));
  • обидві сторони надсилають повідомлення будь-коли, з мінімальними накладними витратами;
  • текст і бінарні дані;
  • перепідключення, «серцебиття», підписки на канали - пишете самі або беруть бібліотеку;
  • потрібен сервер, що тримає тисячі постійних з'єднань, і налаштовані проксі.

Коли що:

Задача Вибір
статус замовлення, сповіщення, прогрес задачі, стрічка подій SSE
потокова відповідь LLM («друкується» по словах) SSE / потоковий fetch
чат, спільне редагування, ігри, присутність користувачів WebSocket
оновлення раз на хвилину звичайне опитування - найпростіше

У Laravel-екосистемі:

  • WebSocket - Laravel Reverb (свій сервер) чи Pusher, на клієнті Laravel Echo; трансляція подій через ShouldBroadcast;
  • SSE - response()->eventStream() у контролері; Livewire має wire:stream для потокового оновлення частини компонента.

Головна перевага SSE - простота: не потрібен окремий сервер і протокол, це звичайний маршрут Laravel. Але кожен відкритий потік тримає процес PHP-FPM зайнятим - під багато одночасних клієнтів потрібні Octane чи окремий сервіс.

Докладніше в документації: Server-Sent Events

Опція credentials визначає, чи надсилає fetch cookies і чи приймає їх з відповіді:

  • 'same-origin' (за замовчуванням) - лише для запитів на той самий домен;
  • 'include' - і для інших доменів (потрібна відповідна CORS-конфігурація сервера);
  • 'omit' - ніколи.

Для звичайного Laravel-застосунку, де сторінки й запити з одного домену, сесійна cookie надсилається автоматично.

CSRF-захист у Laravel перевіряє, що запит, який змінює дані (POST, PUT, PATCH, DELETE), справді надіслано з вашого сайту. Способи передати токен з JavaScript:

1. Заголовок X-CSRF-TOKEN з мета-тегу:

<meta name="csrf-token" content="{{ csrf_token() }}">
await fetch('/orders', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    Accept: 'application/json',
    'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
  },
  body: JSON.stringify(order),
});

2. Заголовок X-XSRF-TOKEN з cookie. Laravel ставить cookie XSRF-TOKEN (зашифрований токен, доступний JavaScript). Axios автоматично читає її і додає заголовок для запитів на той самий домен - тому в Laravel-проєктах з axios про CSRF зазвичай не думають. З fetch це треба робити самому.

3. Поле _token у тілі запиту - якщо відправляється FormData з форми, де є @csrf.

Laravel 13 перевіряє ще й джерело запиту. Middleware PreventRequestForgery пропускає запит без токена, якщо браузер надіслав заголовок Sec-Fetch-Site: same-origin. Сучасні браузери додають його самі, а підробити з іншого сайту неможливо. Токен лишається запасним механізмом для старих браузерів і запитів без цього заголовка.

Помилка 419 (Page Expired / CSRF token mismatch) - токен не передано, він застарів (сесія закінчилася, поки сторінка була відкрита) або сесійна cookie не дійшла. Для довго відкритих сторінок варто обробляти 419: оновити токен чи запропонувати перезавантажити сторінку.

SPA на іншому піддомені зі Sanctum:

await fetch('https://api.example.com/sanctum/csrf-cookie', { credentials: 'include' });

await fetch('https://api.example.com/api/orders', {
  method: 'POST',
  credentials: 'include',
  headers: { 'X-XSRF-TOKEN': decodeURIComponent(getCookie('XSRF-TOKEN')), Accept: 'application/json' },
  body: JSON.stringify(order),
});

Потрібні також supports_credentials у config/cors.php, домен SPA у SANCTUM_STATEFUL_DOMAINS і спільний домен сесійних cookies.

Без Accept: application/json Laravel на помилку валідації відповість редиректом, а не JSON з помилками.

Докладніше в документації: fetch()

Intl - вбудований у браузер і Node.js API інтернаціоналізації. Він знає правила форматування для сотень мов, тож не потрібно писати власні функції чи підключати важкі бібліотеки.

Числа й гроші:

new Intl.NumberFormat('uk-UA', { style: 'currency', currency: 'UAH' }).format(1234567.891);
// "1 234 567,89 ₴"

new Intl.NumberFormat('uk-UA', { notation: 'compact' }).format(1500000);
// "1,5 млн"

new Intl.NumberFormat('uk-UA', { style: 'percent', maximumFractionDigits: 1 }).format(0.1234);

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

Дати:

new Intl.DateTimeFormat('uk-UA', { dateStyle: 'long', timeZone: 'Europe/Kyiv' })
  .format(new Date('2026-10-04T10:00:00Z'));
// "4 жовтня 2026 р."

timeZone важливий: без нього дата форматується в поясі браузера користувача.

Відносний час:

const rtf = new Intl.RelativeTimeFormat('uk', { numeric: 'auto' });
rtf.format(-1, 'day');      // "учора"
rtf.format(3, 'hour');      // "через 3 години"
rtf.format(-5, 'minute');   // "5 хвилин тому"

Множина - найскладніше для української: три форми плюс дроби.

const rules = new Intl.PluralRules('uk');
rules.select(1);    // "one"   -> 1 коментар, 21 коментар
rules.select(2);    // "few"   -> 2 коментарі, 22 коментарі
rules.select(5);    // "many"  -> 5 коментарів, 11 коментарів
rules.select(1.5);  // "other" -> 1,5 коментаря

const forms = { one: 'коментар', few: 'коментарі', many: 'коментарів', other: 'коментаря' };
const label = (n) => `${n} ${forms[rules.select(n)]}`;

Ручне правило «якщо 1 - однина, інакше множина» дає «21 коментарів» і «2 коментарів».

Сортування рядків:

['їжак', 'ґанок', 'яблуко', 'єнот'].sort();
// ["яблуко", "єнот", "їжак", "ґанок"] - за кодами символів

['їжак', 'ґанок', 'яблуко', 'єнот'].sort(new Intl.Collator('uk').compare);
// ["ґанок", "єнот", "їжак", "яблуко"] - за абеткою

Продуктивність: створення форматера відносно дороге - для списків створюйте його один раз і використовуйте format() повторно.

Узгодженість з бекендом: PHP має ті самі правила ICU через розширення intl (NumberFormatter, Number::currency() у Laravel), тож формати на сервері й у браузері збігаються.

Докладніше в документації: MDN: Intl