Query-параметры — данные, которые едут в самом URL после знака вопроса:

https://jsonplaceholder.typicode.com/posts?userId=1&_limit=5

Пары «ключ=значение», разделённые амперсандом. Пара после первого ?, остальные через &. Это самый частый способ уточнить GET-запрос — чего именно вы хотите.

Зачем

В классическом REST URL содержит существительное-ресурс: /posts. Но «все посты» — это часто слишком много. Нужно отфильтровать, ограничить, отсортировать. Для этого и нужны query-параметры:

  • фильтр?status=published&category=css;
  • сортировка?sort=created_at&order=desc;
  • пагинация?page=2&per_page=20;
  • поиск?q=fetch&in=title;
  • выбор полей?fields=id,title,author.

Конкретные имена параметров каждый API выбирает сам. Никакого единого стандарта нет, но шаблоны выше встречаются повсюду.

Простой случай: руками склеить строку

const userId = 1;
const limit = 5;

const url = `https://jsonplaceholder.typicode.com/posts?userId=${userId}&_limit=${limit}`;
// _limit - не баг. В JSONPlaceholder параметры для пагинации и сортировки специально идут с нижним подчёркиванием

const response = await fetch(url);
const posts = await response.json();

Работает. Но если значения содержат пробелы, кириллицу, амперсанды, плюсы, знаки вопроса — пойдут проблемы. Любой такой символ нужно экранировать (заменить на %XX-последовательность). Делать это вручную — больно и легко забыть.

Правильный способ: URLSearchParams

В каждом современном браузере есть встроенный объект URLSearchParams, который собирает строку query-параметров правильно:

const params = new URLSearchParams({
  q: 'fetch & promise',
  category: 'css',
  page: 2,
});

const url = `https://api.example.com/search?${params.toString()}`;
// → https://api.example.com/search?q=fetch+%26+promise&category=css&page=2

const response = await fetch(url);

Все спецсимволы экранируются автоматически. Получившаяся строка валидна, никаких сюрпризов сервер не словит.

Если значение пустое или undefined — параметр всё равно добавится с пустым значением. Если этого не хочется, фильтруем заранее:

const filters = { q: searchInput, category: selectedCategory };
const params = new URLSearchParams();

for (const [key, value] of Object.entries(filters)) {
  if (value) params.append(key, value);
}

Несколько значений одного параметра

Бывает, нужно передать массив значений. Универсального стандарта нет, чаще всего работает повтор ключа:

const params = new URLSearchParams();
params.append('tag', 'css');
params.append('tag', 'html');
params.append('tag', 'js');
// → tag=css&tag=html&tag=js

Некоторые API ждут другой формат — tag[]=css&tag[]=html или tags=css,html,js. Смотрите документацию.

Пагинация: три популярных шаблона

На больших коллекциях сервер не отдаёт всё разом, а отдаёт «страницами». Три самых частых способа.

1. limit + offset:

GET /posts?limit=20&offset=40   ← вернуть 20 записей, начиная с 41-й

2. page + per_page:

GET /posts?page=3&per_page=20   ← то же самое, но в номерах страниц

3. Cursor-based:

GET /posts?after=eyJpZCI6MTIzfQ&limit=20   ← вернуть 20 записей после курсора

Третий способ — самый правильный для больших и часто меняющихся коллекций. Если между запросами добавляются новые записи, limit/offset начинает повторять элементы или пропускать. Cursor этого недостатка лишён, потому что «закладка» привязана не к номеру строки, а к конкретной записи.

На собеседовании могут попросить рассказать разницу между offset и cursor — пригодится.

Ограничения

Query-параметры удобны, но не подходят для:

  • Большого объёма данных. Браузеры ограничивают длину URL примерно 2 000–8 000 символов. Если нужно отправить килобайт JSON — кладите в тело POST-запроса.
  • Чувствительных данных. URL попадает в логи серверов, истории браузера, заголовок Referer. Пароли, токены и другие секреты — никогда в query.
  • Действий, меняющих данные. GET — safe-метод (см. главу 3.2). Удаление, создание, обновление через query-параметры на GET-эндпоинт — антипаттерн.

Для этих случаев используем тело запроса — следующая глава.