Чтобы fetch сделал не GET, а POST, нужно передать вторым аргументом объект с опциями.

Базовый POST с JSON

const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    title: 'Привет, мир',
    body: 'Это мой первый пост.',
    userId: 1,
  }),
});

if (!response.ok) throw new Error(`HTTP ${response.status}`);
const created = await response.json();
console.log(created); // {id: 101, title: ..., body: ..., userId: 1}

Разберём по строкам.

Поле method

Имя HTTP-метода. Если опустить — по умолчанию GET. Для POST, PUT, PATCH, DELETE — указываем явно.

Имя метода чувствительно к регистру: 'POST' работает, 'post' — нет. По соглашению пишем заглавными.

Поле headers

Заголовки запроса как объект. Самый важный для POST с JSON — Content-Type: application/json. Он сообщает серверу, что в теле лежит JSON и его нужно парсить именно так. Без этого заголовка многие сервера откажут или поймут тело как чистый текст.

Что ещё кладём в headers:

  • Authorization — если API требует токен. Подробно — в модуле 7.
  • Accept — формат ожидаемого ответа.
  • Любые кастомные заголовки вида X-Request-IDX-Client-Version — если API их ждёт.

Поле body

body — то, что поедет в тело запроса. Должно быть строкой (или одним из специальных типов: FormData, Blob, URLSearchParams — о них ниже).

JS-объект напрямую положить нельзя. Если сделать body: {title: 'X'}fetch приведёт объект к строке через toString() — и пришлёт серверу буквальную строку "[object Object]". Знаменитый баг джунов; ловится один раз — и больше никогда.

Правильно — body: JSON.stringify(объект). Эта пара (Content-Type: application/jsonJSON.stringify) встречается практически в каждом POST/PUT/PATCH-запросе из JS. Привыкайте писать вместе.

Что вернёт сервер

На удачный POST в коллекцию обычно приходит код 201 (Created). В теле — созданный ресурс, с присвоенным сервером id. В заголовке Location — URL созданного ресурса.

Иногда возвращают 200 (если сервер считает, что ресурс «применён», а не «создан») или 204 (если ничего возвращать не нужно). Конкретный код описан в документации API.

Отправка формы вместо JSON

Не каждое API ждёт JSON. Иногда (особенно у старых сервисов и форм авторизации в OAuth) тело должно быть в формате «url-encoded form» — тот же синтаксис, что у query-параметров, только в теле.

const response = await fetch('https://example.com/login', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded',
  },
  body: new URLSearchParams({
    username: 'anna',
    password: '12345',
  }),
});

А если в форме есть файлы — нужна FormData:

const data = new FormData();
data.append('title', 'Аватарка');
data.append('avatar', fileInput.files[0]);

const response = await fetch('/upload', {
  method: 'POST',
  body: data,
  // НЕ ставим Content-Type вручную! Браузер сам поставит
  // multipart/form-data с правильным boundary.
});

Важный нюанс с FormData: не ставьте Content-Type руками. Браузер сам сформирует строку вида multipart/form-data; boundary=----WebKitFormBoundary..., в которой boundary — уникальный разделитель кусков. Если поставите свой, серверу будет нечем парсить и он развалится.

Краткая шпаргалка

Что отправляем Content-Type body
JSON application/json JSON.stringify(obj)
Форма без файлов application/x-www-form-urlencoded new URLSearchParams(obj)
Форма с файлами не ставим вручную new FormData()
Простой текст text/plain 'строка'
Бинарные данные application/octet-stream Blob или ArrayBuffer

В 95% работы с REST API используется первый вариант — JSON. Остальные пригодятся в специальных случаях.