Fetch для QA: как тестировать API прямо в DevTools, не открывая Postman

Недавно у меня была итерация тестирования. Задача — проверить функционал, для которого нужно было создать 50+ сущностей. Однотипных, с разными названиями.
Обычно такое делаю через интерфейс. Открываю форму, заполняю поля, сохраняю. Потом ещё раз. И ещё. Пятьдесят раз. Я замерял: на одну сущность уходит примерно 20 секунд. Умножаем на 50 и получается больше 15 минут однотипных действий. А времени на итерации всегда мало.
Я уже собирался начать делать это руками, но вспомнил про fetch. Ту самую функцию, которая живёт в DevTools на вкладке Console. Пару минут на скрипт и 50 сущностей созданы за минуту. Итог: я сэкономил больше 15 минут. И заодно освоил инструмент, который теперь использую регулярно.
В статье расскажу, как это работает. Покажу шаблон запроса, научу копировать его из DevTools, отправлять пачками и параллельно. Будем использовать только браузер.
Немного про Fetch
Fetch — это встроенная в браузер функция JavaScript для отправки HTTP‑запросов. Ставить ничего не нужно: она уже есть в DevTools на вкладке Console. В Postman ты жмёшь Send. Здесь ты сам пишешь «Send» — вот так:
fetch('https://test.com/api/users')
Синтаксис минимальный, ну почти:) Для многих задач хватит одного шаблона, который мы разберём в следующем блоке. Но есть два момента, о которых лучше знать заранее.
Первый: fetch возвращает Promise. Когда ты пишешь fetch(url), JavaScript не ждёт, пока сервер ответит. Он сразу отдаёт объект Promise — этакую «расписку»: мол, запрос ушёл, ответ будет, но чуть позже. Представь: ты нажал Enter, а ответа нет. Секунду нет. Две нет. Это не баг — это Promise в действии. Дождаться ответа можно через.then() или await. Возможно, тебе покажется непонятно, но в шаблоне это будет одна готовая строчка. Просто скопируешь и всё.

pending (ожидает), fulfilled (выполнено), rejected (отклонено). Пока запрос идет на сервер и обратно — Promise в состоянии pending. Как только ответ пришёл — становится fulfilled. Если что‑то сломалось — rejected.Второй: для fetch успех — это любой ответ от сервера. Даже 404 или 500. Функция не «упадёт» и не вернет ошибку — спокойно отдаст ответ. Запомни простое правило: fetch — «оптимист», он рад любому ответу. А мы, QA, — пессимисты:) В шаблоне мы отдельно выведем status.

Всё, с теорией закончили. Дальше практика: покажу шаблон POST‑запроса, который за пару минут можно адаптировать под любой свой эндпоинт.
Шаблон fetch для POST‑запроса
Теперь, когда ты понимаешь (надеюсь, что я понятно объяснил), что такое fetch, давай напишем первый рабочий запрос. Я напишу пример для POST запроса. Тебе останется подставить свои значения.
fetch(‘<URL_вашего_эндпоинта>’, {method: ‘POST’,
headers: {
‘Content-Type’: ‘application/json’,
‘Authorization’: ‘<ваш_токен>’ // зависит от типа авторизации
},
body: JSON.stringify({ // здесь передаем тело запроса
key_1: value_1,
key_2: value_2
})})
.then(res => {
console.log(‘Status:’, res.status);
return res.json(); })
.then(data => console.log(‘Body:’, data))
.catch(err => console.error(‘Ошибка:’, err));
Скопируй, подставь свои значения (URL, Headers и Body), вставь в консоль DevTools (F12 → Console) и нажми Enter. Ты увидишь что‑то вроде:Status: 201
Body: <здесь ответ сервера>

Готово. Ты отправил POST‑запрос и получил ответ без Postman.
Про токен и тип авторизации:
В шаблоне стоит 'Authorization': '<ваш_токен>'. Тип зависит от того, какой тип авторизации.
Пару примеров типов:
Тип | Как выглядит |
Basic Auth | Authorization: Basic <логин:пароль в base64> |
Bearer Token | Authorization: Bearer <токен> |
API Key | X‑API‑Key: <ключ> |
Как узнать, какой у тебя: Посмотреть в доке или в DevTools → Network → любой запрос фронтенда → Request Headers. Смотри, что стоит рядом с Authorization или похожим заголовком. Если метод публичный и авторизация не нужна, то строку с Authorization можно не передавать.
А где вообще запускать fetch запрос? Во первых в консоле девтулза и во вторых запускать можно на любой странице. Fetch всё равно с какой страницы шлешь запрос. Но есть нюанс — CORS. Если ты на одном сайте, а запрос шлёшь на другой, браузер может заблокировать ответ. Увидишь ошибку вроде:
Access to fetch at '<URL_вашего_эндпоинта>'
from origin 'https://google.com' has been blocked by CORS policy
Поэтому желательно придерживать правила: открывай Console на том же домене, к которому идут запросы. Тестируешь test.com — сиди на test.com.
Разберём по строчкам шаблон fetch запроса
fetch('<URL_твоего_эндпоинта>', {аргумент №2})
Первый аргумент — это адрес куда шлем запрос (в формате строки). Второй — настройки. Если не указывать второй аргумент, то fetch отправит GET запрос.
method: 'POST'
Тип запроса. Если не указать method, то fetch отправит GET. Здесь мы явно указываем POST.
headers: { ... }
Набор заголовков зависит от того, как настроен API в твоём проекте. Host и Content‑Length можно не передавать. Но эти два заголовка нужно передать:
Заголовок | Обязателен? |
Content‑Type: application/json | Да, если в методе передаем тело |
Authorization: <тип> <токен> | Да, если метод приватный |
body: JSON.stringify({...})
Тело запроса. JSON.stringify превращает объект в строку — HTTP передаёт только текст.
.then(res => ...)
Сюда попадает объект Response. У него есть:
res.status — код ответа (201, 404, 500)
res.ok — true, если статус в диапазоне 200–299
return res.json()
Тело ответа приходит строкой. res.json() парсит эту строку и превращает в объект — с ним уже можно работать: data.id, data.name
.then(data => ...)
Здесь data — готовый объект с ответом. Выводим его в консоль.
.catch(err => ...)
Ловим сетевые ошибки: нет интернета, сервер недоступен и тд. Помнишь из блока 2? Ошибки 4xx и 5xx сюда не попадают, так как fetch считает их успешными.
Как адаптировать под другие эндпоинты? Меняешь три вещи:
Что меняешь | Пример |
URL | '<URL_вашего_эндпоинта>' → адрес твоего API |
method | 'POST' → 'PUT', 'PATCH' или 'DELETE' (и так далее) |
body | Свое тело: { name: '...', email: '...' } |
Как ранее писал, для GET‑запроса второй аргумент не нужен, но если нужна авторизация, заголовок остаётся:
fetch('<URL_вашего_эндпоинта>', {
headers: {
'Authorization': 'Bearer <ваш_токен>'
}
})
.then(res => res.json())
.then(data => console.log(data));
Практика: находим запрос на реальном сайте
Для практики возьмём любой сайт, где есть формы или кнопки, создающие сущности. Я выбрал тренажёр для QA. Доступа к документации API у меня нет. Поэтому сначала выполню действие в приложении, а потом соберу fetch‑запрос по тому, что увижу в DevTools.
Если у тебя есть доступ к Swagger, то можешь собрать fetch по шаблону, взяв данные из доки. А если доступа нет, выручит Network в DevTools.
Шаг 1. Открой DevTools
Заходи на выбранный сайт. Открой DevTools — F12 или Ctrl+Shift+I (на Mac — Cmd+Option+I). Перейди на вкладку Network.
Ещё нажми на иконку «перечёркнутый круг» (Clear), чтобы очистить логи от лишнего.

Шаг 2. Сделай действие, которое создаёт сущность
Выполни любое действие, которые триггерит POST запрос. Например:
добавь нового пользователя
создай задачу
отправь сообщение
Теперь найди запрос в Network.

Шаг 3. Найди POST‑запрос и скопируй его fetch
В Network появится одна или несколько строк. Тебе нужна та, где в столбце Method стоит POST. Кликни по ней правой кнопкой мыши → Copy → Copy as fetch.

Всё. DevTools сам сгенерировал готовый fetch‑запрос. С URL, заголовками и телом. Осталось вставить его в Console.
Шаг 4. Вставь в Console и запусти
Перейди на вкладку Console и вставь скопированное. Выглядеть будет примерно так:
fetch("https://site.com/api/users", {
"headers": {
"accept": "application/json, text/plain, /",
"authorization": "Bearer eyJhbGciOi...",
"content-type": "application/json"
},
"body": "{\"name\":\"Test User\",\"email\":\"test@example.com\"}",
"method": "POST"
});
Нажми Enter и запрос отправляется. Для примера можешь зайти во вкладку Network и проверить этот запрос.
Шаг 5. Добавь вывод ответа
Скопированный запрос не выводит ответ. Он просто отправляет данные и всё. Ты не увидишь ни статус‑код, ни тело ответа. Поэтому допиши в конец цепочку и не забудь обновить на уникальные тестовые данные в теле:
fetch("https://site.com/api/users", {
"headers": {
"accept": "application/json, text/plain, /",
"authorization": "Bearer eyJhbGciOi...",
"content-type": "application/json"
},
"body": "{\"name\":\"Test User\",\"email\":\"test@example.com\"}",
"method": "POST"
})
.then(res => {
console.log('Status:', res.status);
return res.json();
})
.then(data => console.log('Body:', data))
.catch(err => console.error('Ошибка:', err));
Вот теперь в консоли появятся Status: 201 и Body: {...}. Всё как в блоке 3. Шаблон тот же, просто данные DevTools подставил сам.

Зачем это нужно на практике? Дальше ты можешь:
Проверить валидацию. Убери из body обязательное поле — увидишь, вернёт ли сервер 400 и понятное сообщение.
Проверить граничные значения. Отправь строку 101 символов туда, где ожидается 100 — сервер должен вернуть ошибку.
Проверить авторизацию
Повторить запрос 50 раз
и тд
Что дальше. Ты умеешь находить запрос в Network и отправлять его через fetch. Дальше как отправлять несколько запросов подряд, когда второй зависит от первого.
Запрос А → запрос Б
Классический пример: создаёшь сущность, получаешь её id, используешь этот id в следующем запросе. Пример: создаём сущность и проверяем, что она создалась.
Предположим, что есть два эндпоинта:
POST /api/entities — создаёт сущность, возвращает её id
GET /api/entities/{id} — возвращает сущность по id
Задача: создать сущность и убедиться, что запись сохранилась.
Первый запрос — создаём сущность:
fetch('https://site.com/api/entities', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Basic <ваш_токен>'
},
body: JSON.stringify({
key_1: value_1,
key_2: value_2
})
})
.then(res => res.json())
.then(data => console.log('Создана:', data))
.catch(err => console.error('Ошибка:', err));
В ответе придёт что‑то вроде:
Создана: {id: 47, key_1: "value_1", key_2: "value_2", ...}
Вот этот id: 47 нам и нужен для второго запроса.
Тут возникает вопрос: как передать id из первого запроса во второй? В примере ключ называется id, но у другого API он может называться иначе: uuid, key, entityId. Смотри, что приходит в ответе. Можно написать так, чтобы id подставлялся автоматически. Через await:
// Создаём сущность
const createRes = await fetch('https://site.com/api/entities', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Basic <ваш_токен>'
},
body: JSON.stringify({
key_1: value_1,
key_2: value_2
})
});
const created = await createRes.json(); // тело ответа в created — здесь лежит id (или uuid, key)
console.log('Создана:', created);// Запрашиваем её же по её id
const getRes = await fetch(
https://site.com/api/entities/${created.id},
{
headers: {
'Authorization': 'Basic <ваш_токен>'
}
}
);
const found = await getRes.json();
console.log('Найдена:', found);

Почему тут await, а не.then(). Fetch возвращает Promise. В блоках 3 и 4 мы ждали его через.then(). Это классический способ. Но когда запросов несколько и они зависят друг от друга,.then() становится громоздким: код растёт и читать становится тяжело. А await позволяет писать код сверху вниз, как обычные инструкции:
const created = await createRes.json();
// используем created.id в следующем запросе
Читается проще. Особенно когда запросов не два, а пять или десять. В блоке 6 это пригодится.
Массовая отправка запросов через fetch
Сделаем это через цикл:
const TOKEN = 'Basic <ваш_токен>';
const URL = 'https://site.com/api/entities';
for (let i = 1; i <= 50; i++) {
const res = await fetch(URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': TOKEN
},
body: JSON.stringify({
name: Сущность ${i},
description: Описание номер ${i},
})
});
const data = await res.json();
console.log(Запрос ${i}:, res.status, data);
await new Promise(resolve => setTimeout(resolve, 1000));
}
Запускаем и последовательно отправляются запросы:
Запрос 1: 201 {id: 51, name: "Сущность 1", ...}
Запрос 2: 201 {id: 52, name: "Сущность 2", ...}
Запрос 3: 201 {id: 53, name: "Сущность 3", ...}
...
Запрос 50: 201 {id: 101, name: "Сущность 50", ...}

Разберём по строчкам простыми словами.
const TOKEN = ... и const URL = ...
Выносим повторяющиеся значения в переменные. Так код короче, и менять в одном месте значение быстрее.
for (let i = 1; i <= 50; i++)

Цикл, который выполнится 50 раз. i — это счётчик: начинается с 1, увеличивается на 1 на каждом шаге, останавливается на 50. Внутри цикла код, который отправит запрос. То есть 50 итераций = 50 запросов.
name: Сущность ${i}
Подставляет число i в название. Получаются: при первой итерации = «Сущность 1», при второй итерации = «Сущность 2» и так далее. Так каждая запись уникальна, иначе сервер вернет ошибку..
description: Описание номер ${i}
Здесь аналогично как для ключа name
await fetch(...)
Ждём, пока запрос выполнится, и только потом идём дальше. Поэтому запросы уходят по очереди: второй не отправится, пока не завершится первый.
console.log(Запрос ${i}:, res.status, data)
Выводим номер запроса, статус‑код и тело ответа. Сразу видно какой запрос упал.
await new Promise(resolve => setTimeout(resolve, 1000))
Пауза на 1 секунду между запросами. Строка делает простую вещь: ждёт 1000 миллисекунд перед следующим запросом.
Можно было бы просто написать «готово» в конце. Но тогда ты не поймёшь, что произошло, если где‑то ошибка.
С выводом каждого ответа видно:
Все 50 строк зелёные — всё хорошо.
На 23-й итерации 500 — сущность не создалась.
Скрипт завис на 30-й — возможно, сервер тормозит или отвалился.
Один момент
Если запустишь цикл на 50 запросов без остановки, то он выполнится за минуту. Но если что‑то пойдёт не так, остановить его сложно: Console не всегда прерывает await‑цикл по Esc. Поэтому на первых прогонах ставь небольшое число — 5 или 10. Убедись, что всё работает, потом увеличивай до 50+.
Параллельная отправка запросов через fetch
До этого мы отправляли запросы по очереди: один закончился, начался следующий. Но так бывает не всегда. Два пользователя могут нажать кнопку одновременно и оба запроса уйдут на сервер в одну и ту же секунду.
В JavaScript есть Promise.all. Он запускает несколько промисов сразу и ждёт, пока все завершатся.
const TOKEN = 'Basic <ваш_токен>';
const URL = 'https://site.com/api/entities';
const res1 = fetch(URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': TOKEN // Передаешь токен Пользователя №1
},
body: JSON.stringify({
name: 'Пользователь 1',
description: 'Создано первым'
})
});
const res2 = fetch(URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': TOKEN // Передаешь токен Пользователя №2
},
body: JSON.stringify({
name: 'Пользователь 2',
description: 'Создано вторым'
})
});
const [r1, r2] = await Promise.all([res1, res2]);
console.log('Первый:', r1.status, await r1.json());
console.log('Второй:', r2.status, await r2.json());
Что тут происходит:
const res1 = fetch(...)
Запускаем первый запрос и кладём промис в res1. Обрати внимание: без await. Мы не ждём результат — просто отправляем запрос и идём дальше.
const res2 = fetch(...)
То же самое для второго запроса. Оба уходят почти одновременно.
await Promise.all([res1, res2])
Ждём оба запроса. Promise.all берёт массив промисов и возвращает массив результатов, когда все они завершатся.
const [r1, r2] = ...
Разбираем массив на две переменные. r1 — ответ первого запроса, r2 — второго.
Зачем это нужно на практике? Представь: два пользователя одновременно нажимают «Создать». Или один и тот же пользователь отправляет форму дважды и кликнул два раза.
Проверить такие сценарии вручную сложно. А с Promise.all это сделать легко. Ты воспроизводишь скрипт и смотришь, как поведёт себя система.
Итог
Мы прошли путь от одного запроса до последовательной и параллельной отправки. Давай соберём всё вместе.
Что ты теперь знаешь
Отправлять один запрос через консоль девтулза
Копировать запрос из DevTools — через Copy as fetch
Связывать два запроса, когда второй зависит от первого.
Отправлять много запросов через цикл.
Отправлять два запроса одновременно через Promise.all.
Fetch в DevTools — не является полной заменой Postman. Это быстрый инструмент под рукой. Когда нужно проверить что‑то здесь и сейчас, не открывая лишних окон.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.