Как отправить форму через fetch: JSON, FormData и файлы
- 25 сентября 2026
Для отправки формы сначала уточните, какой формат принимает API. В учебном примере подготовим два маршрута: JSON для полей и FormData для полей с необязательным файлом. Затем проверим ответ сервера.
Соберём учебную заявку: почта, число мест и согласие получать новости. Вложение необязательно. Для проверки используйте вымышленные данные, например student@example.test, и небольшой текстовый файл без личной информации. Базовый принцип запроса уже разобран в статье «Как работать с fetch»; здесь внимание на том, что именно отправит форма.
Уточните формат у API
Для примера договоримся о двух адресах. Это условные маршруты вашего учебного бэкенда, их нужно реализовать в проекте или заменить своими. Они не существуют на сайте Академии.
| Адрес и метод | Тело запроса | Успешный ответ |
|---|---|---|
POST /api/request-json | JSON: email со строкой, quantity с числом от одного до 99, newsletter с логическим значением true или false. | JSON с подтверждением или 204 без тела. |
POST /api/request-form | multipart/form-data: те же поля строками, необязательное attachment с файлом. newsletter принимает строки 'true' и 'false'. | JSON с подтверждением или 204 без тела. |
В учебном контракте файл ограничен двумя МиБ: это 2 × 1024 × 1024 байта. На сервере ограничение тоже проверяют. Значение в браузере можно изменить, а запрос — отправить вообще без формы.
Маршруты, названия полей и ответ выбирает API. Если сервер принимает только JSON, переключение на FormData не добавит ему загрузку файлов. Потребуется поддержка на стороне бэкенда. Документация Fetch описывает допустимые тела запроса, но договор с вашим сервером она не заменяет.
Подготовьте поля с name
Поместите форму в index.html. Скрипт подключим отдельным модулем; страница и API в примере работают на одном origin — с одинаковыми протоколом, хостом и портом.
<form id="request-form">
<label>Почта <input type="email" name="email" required></label>
<label>Места <input type="number" name="quantity"
min="1" max="99" step="1" value="1" required></label>
<label><input type="checkbox" name="newsletter"> Получать новости</label>
<label>Файл до 2 МиБ <input type="file" name="attachment"></label>
<label>Формат
<select id="format">
<option value="json">JSON, без файла</option>
<option value="multipart">FormData, с файлом или без</option>
</select>
</label>
<button type="submit">Отправить учебную заявку</button>
</form>
<p id="status" role="status"></p>
<script type="module" src="./main.js"></script>
name задаёт имя поля в FormData. Одного id недостаточно. В список данных также не попадут выключенные поля с disabled; неотмеченный чекбокс отсутствует целиком. Эти правила описаны в руководстве по FormData.
Есть ещё одна деталь: type="number" помогает проверить ввод в форме, но прочитанное значение остаётся строкой. Поэтому для JSON число преобразуем явно, а состояние чекбокса определим по наличию поля.
В файле request.js напишите функцию чтения:
export function readFields(data) {
const email = data.get('email');
const rawQuantity = data.get('quantity');
if (typeof email !== 'string' || email.trim() === '') {
throw new Error('Укажите почту');
}
if (typeof rawQuantity !== 'string' || !/^\d+$/.test(rawQuantity)) {
throw new Error('Количество должно быть целым числом от 1 до 99');
}
const quantity = Number(rawQuantity);
if (!Number.isSafeInteger(quantity) || quantity < 1 || quantity > 99) {
throw new Error('Количество должно быть целым числом от 1 до 99');
}
return {email: email.trim(), quantity, newsletter: data.has('newsletter')};
}
Регулярное выражение принимает запись количества десятичными цифрами. Пустая строка и 1.5 попадут в ошибку до преобразования. Проверка адреса почты в интерфейсе остаётся за type="email", а сервер должен проверить адрес по своим правилам.
Соберите JSON или FormData
Добавьте в request.js функцию выбора тела, которая использует readFields из предыдущего блока и готовит параметры запроса для выбранного формата.
export function buildRequest(data, format) {
const fields = readFields(data);
const attachment = data.get('attachment');
const hasFile = attachment instanceof File && attachment.name !== '';
if (hasFile && attachment.size > 2 * 1024 * 1024) {
throw new Error('Файл должен быть не больше 2 МиБ');
}
if (format === 'json') {
if (hasFile) {
throw new Error('Для выбранного файла переключите формат на FormData');
}
return {
url: '/api/request-json',
options: {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(fields),
},
};
}
if (format !== 'multipart') {
throw new Error('Неизвестный формат');
}
data.set('email', fields.email);
data.set('quantity', String(fields.quantity));
data.set('newsletter', String(fields.newsletter));
if (!hasFile) data.delete('attachment');
return {url: '/api/request-form', options: {method: 'POST', body: data}};
}
JSON получает выбранные поля. Объект сериализуем через JSON.stringify. Отмеченный чекбокс превратится в true, снятый — в false. Файл в такую заявку не пропускаем: стандартная сериализация объекта File в JSON не передаст его содержимое. Молча отбросить выбранное вложение было бы неприятным сюрпризом для пользователя.
В варианте FormData файл остаётся частью тела. Число и согласие записываем строками по нашему контракту. Когда файл не выбран, удаляем пустое поле attachment; настоящий файл нулевого размера с именем при этом допустим.
Перед следующим объяснением предскажите, сможет ли сервер разобрать тело FormData, если вручную поставить заголовок Content-Type: multipart/form-data без параметра boundary. Сформулируйте причину, затем сверьте её с объяснением.
Браузер сам добавит multipart/form-data с параметром boundary, который разделяет части тела. Если вручную поставить только multipart/form-data, серверу не хватит границы для разбора. На это отдельно указывает MDN.
Для самопроверки отправьте заявку текущим кодом и найдите в Network полный Content-Type: в нём должен быть boundary, хотя мы не задавали заголовок в options.
Проверьте статус ответа
Загрузите данные через следующую функцию из request.js. Успешный ответ по нашему договору либо содержит JSON, либо имеет статус 204.
export async function sendRequest(url, options, request = fetch) {
const response = await request(url, options);
if (!response.ok) {
throw new Error(`Ошибка HTTP: ${response.status}`);
}
if (response.status === 204) return null;
return response.json();
}
Для ответа 400 или 500 промис fetch может успешно завершиться: ответ сервера получен. Поэтому проверяем response.ok сами. При 204 тела нет, и вызывать json() не нужно. Если другой успешный статус пришёл с пустым телом, наш контракт нарушен: разбор JSON завершится ошибкой.
Теперь подключите функции в main.js. Элементы формы уже должны быть в документе, как в HTML выше.
import {buildRequest, sendRequest} from './request.js';
const form = document.querySelector('#request-form');
const format = document.querySelector('#format');
const status = document.querySelector('#status');
const button = form.querySelector('button[type="submit"]');
let isSending = false;
form.addEventListener('submit', async (event) => {
event.preventDefault();
if (isSending) return;
try {
const data = new FormData(form);
const {url, options} = buildRequest(data, format.value);
isSending = true;
button.disabled = true;
status.textContent = 'Отправляем…';
await sendRequest(url, options);
status.textContent = 'Сервер принял учебную заявку';
} catch (error) {
status.textContent = error instanceof Error
? `Не удалось подтвердить отправку: ${error.message}`
: 'Не удалось подтвердить отправку';
} finally {
isSending = false;
button.disabled = false;
}
});
preventDefault() вызываем до первого await, чтобы браузер не отправил форму обычным способом. Это нужно сделать сразу. Пока ждём ответ сервера, флаг и выключенная кнопка защищают от повторного запуска обработчика, который иначе создаст ещё один запрос с той же заявкой. Блок finally возвращает кнопку в работу и после успеха, и после ошибки.
Поля не очищаем: пользователь может проверить ввод, если сервер отклонил заявку. Сетевой сбой ещё не доказывает, что сервер ничего не получил, поэтому автоматическую повторную отправку здесь не делаем. Для рабочего сервиса защиту от повторных заявок согласуют с бэкендом.
Открывайте страницу через сервер разработки по HTTP, который обслуживает форму и два учебных маршрута. Для API на другом origin потребуется настройка CORS на сервере. mode: 'no-cors' не решает задачу чтения ответа: такой ответ может оказаться непрозрачным для JavaScript, и обработчик не сможет прочитать подтверждение отправки. Проверьте адрес сервера.
Посмотрите настоящий запрос
Откройте инструменты разработчика, вкладку Network, и отправьте заявку в режиме JSON. В теле должны быть число quantity и логическое значение newsletter. Снимите чекбокс и повторите: ожидается false, поле не исчезает.
После переключения на FormData выберите файл. В запросе появятся отдельные части с именами полей; у Content-Type будет параметр boundary. Проверьте имя вложения. Если сервер возвращает ошибку разбора multipart, сначала посмотрите именно заголовок, затем имена полей.
Для небольшой доработки добавьте поле comment. Подсказка: JSON должен получить свойство со строкой, а FormData соберёт поле с name="comment". Отправьте текст с переносом строки. Сервер должен получить весь комментарий, включая часть после переноса; проверьте её в принятом теле запроса. Отсутствия ошибки недостаточно.
Проверьте форму с ошибкой
Самостоятельно настройте учебный маршрут так, чтобы он возвращал 400, затем 204. В первом случае должно появиться сообщение с кодом HTTP; во втором — подтверждение без ошибки разбора JSON. Остановите сервер и убедитесь, что кнопка снова доступна, а введённые данные остались на месте.
Для системной практики работы с интерфейсами есть курс «JavaScript. Профессиональная разработка веб-интерфейсов». Начать можно с проверки своего запроса: сравните тело в Network с договором API.
Два вопроса для самопроверки: что получит сервер, если снять чекбокс и отправить необработанный FormData? И почему выбранный файл нельзя перенести в JSON тем же вызовом JSON.stringify, что и почту?
«Доктайп» — журнал о фронтенде. Читайте, слушайте и учитесь с нами.