До того как начать кодить, разложим, что именно собираем.

Что должно получиться

Маленькое веб-приложение из одной страницы, в котором:

  • сверху — форма с инпутом и кнопкой «Добавить»;
  • под ней — список задач с чекбоксом «выполнено» и кнопкой удаления у каждой;
  • под списком — небольшая статусная строка («Загружаю...», «Ошибка», «Нет задач»).

Список приходит с сервера. Добавление, удаление, переключение — через fetch.

Какие endpoint используем

Что делаем Метод URL
Загрузить список GET https://jsonplaceholder.typicode.com/todos?_limit=10
Создать задачу POST https://jsonplaceholder.typicode.com/todos
Переключить статус PATCH https://jsonplaceholder.typicode.com/todos/<id>
Удалить задачу DELETE https://jsonplaceholder.typicode.com/todos/<id>

Разметка

Самая базовая. Никаких фреймворков, чистый HTML и одна функция-рендерер.

<div class="app">
  <h1>Мои задачи</h1>

  <form id="form">
    <input id="input" placeholder="Новая задача..." required />
    <button type="submit">Добавить</button>
  </form>

  <ul id="list"></ul>

  <p id="status">Загружаю...</p>
</div>

Общая утилита для API

Чтобы не писать одну и ту же проверку response.ok в каждом запросе, вынесем обёртку (мы уже встречали этот паттерн в модуле 6):

const BASE = 'https://jsonplaceholder.typicode.com';

async function api(path, options = {}) {
  const url = `${BASE}${path}`;
  let response;

  try {
    response = await fetch(url, {
      ...options,
      headers: {
        'Content-Type': 'application/json',
        ...(options.headers || {}),
      },
    });
  } catch {
    throw new Error('Нет связи с сервером');
  }

  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }

  // 204 No Content (после DELETE) — тела нет, парсить нечего.
  if (response.status === 204) return null;

  return response.json();
}

Что покрывает эта обёртка:

  • склейка с базовым URL — не повторяем https://jsonplaceholder... в каждом месте;
  • дефолтный Content-Type: application/json — не забываем на POST/PATCH;
  • трансформация сетевых ошибок в человеческое сообщение;
  • проверка response.ok с throw;
  • защита от 204 No Content — не падаем на парсинге пустого тела.

Со всем этим запросы дальше будут писаться короче — одна строка на каждый.

Состояние приложения

Для такого мини-проекта хватит одной переменной — массива задач:

let todos = [];

Загрузили с сервера — присвоили. Добавили — сделали push. Удалили — отфильтровали. После каждого изменения — зовём render(), который перерисовывает список из текущего todos.

В настоящем проекте такое состояние держат в React, Vue или сторе. Для учебной цели хватит модульной переменной.

В следующей главе пишем функцию loadTodos: один GET-запрос, который заполняет todos при загрузке страницы. Это самая лёгкая часть проекта — с неё и начнём.