commit 8e95dc7f18d2676c1d7a71515e0e0c30d8d6de14 Author: Nikolai Papin Date: Sat Dec 20 02:47:45 2025 +0300 initial commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..464b76b --- /dev/null +++ b/README.md @@ -0,0 +1,685 @@ +# Разрабатываем frontend-приложение и соединяем его с backend +Для секции "Веб-разработка: пример Fullstack" + +## Введение + +На прошлом занятии мы поработали с Node.js и создали первое Web API. Мы реализовали API калькулятора и заметок, в процессе поработав с HTTP запросами. Теперь нам необходимо попытаться связать API фронтенда с API бекенда. + +Для данной работы создан отдельный репозиторий по ссылке: https://git.weirdcat.su/ddi03/notes_app. Это очень простое приложение для управления заметками. Здесь уже реализована backend-часть и основа frontend-сервера. Мы рассмотрим, как они устроены, после чего реализуем требуемые страницы и их логику на JavaScript, чтобы можно было делать запросы на backend для подтягивания данных. + +Предполагается, что вы уже знакомы с HTML, CSS, изучили базовую работу с Git и помните, как работает простейшее API на Node.js. + +--- + +## 📘 **Урок 1: Загрузка проекта и подготовка к работе** +### **Цель урока**: Загрузить из удаленного репозитория изначальные файлы, подготовиться к работе с проектом и ознакомиться с работой серверов frontend и backend + +### **Практические шаги**: + +1. Перейдите в папку, куда вы хотите поместить проект из удаленного репозитория. Скачать репозиторий можно с помощью команды: +```bash +git clone https://git.weirdcat.su/ddi03/notes_app +``` +2. Перейдите в папку с приложением `notes_app`. В ней вы можете видеть уже созданную файловую структуру. +3. Создайте ветку `develop` и перейдите в нее. +4. Выполните `npm install` в папках `backend` и `frontend`, т.к. в свежем репозитории еще не установлены требуемые зависимости. + +Обратите внимание, что у нас имеются папки для frontend и для backend. Как мы помним из предыдущего урока, frontend и backend в клиент-серверной архитектуре функционируют как два разных приложения. Несмотря на то, что backend и frontend разделены, мы решили использовать один репозиторий, хотя можно было бы использовать второй. +``` +├── backend +│   ├── package.json +│   ├── package-lock.json +│   └── src +│   ├── app.js +│   ├── server.js +│   └── swagger.js +└── frontend + ├── package.json + ├── package-lock.json + └── src + ├── public + │   ├── css + │   │   ├── normalize.css + │   │   ├── status.css + │   │   └── style.css + │   ├── html + │   │   ├── 404.html + │   │   ├── 500.html + │   │   ├── index.html + │   │   └── status.html + │   ├── images + │   │   └── logo.svg + │   └── js + │   └── helloWorld.js + └── server.js +``` +Наши backend и frontend оба написаны на Node.js. Задача backend-сервера - обрабатывать бизнес-логику. Рассмотрим, что делает сервер frontend. + +Задача frontend-сервера - выдавать пользователю статические файлы по запросу. Статическими файлами называют те файлы, которые хранятся на фронтенд-сервере и не изменяются. Это страницы HTML, CSS, скрипты JS, картинки, файлы, шрифты и так далее. Фронтенд-сервер необходим, т.к. без него пользователю неоткуда получить файлы страниц и их наполнение. +``` +└── frontend + ├── package.json + ├── package-lock.json + └── src + ├── public + │   ├── css + │   │   ├── normalize.css + │   │   ├── status.css + │   │   └── style.css + │   ├── html + │   │   ├── 404.html + │   │   ├── 500.html + │   │   ├── index.html + │   │   └── status.html + │   ├── images + │   │   └── logo.svg + │   └── js + │   └── helloWorld.js + └── server.js +``` +В файле `server.js` у нас имеется блок кода, в котором мы говорим frontend-серверу, из какой папки нужно сервировать статические файлы для пользователя. для нашего приложения express создаем обработчик `express.static`, который по запросу пользователя выдает любой файл из соседней папки `public`: + +```js +import { dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const app = express(); + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +// Берем путь до текущей папки .../backend/src/public +app.use(express.static(join(__dirname, 'public'))); +``` + +Теперь если пользователь откроет в браузере на нашем сайте URL `/css/normalize.css`, то сможет получить находящися в папке public соответствующий файл. Это работает аналогично и для всех других файлов: `public/html/status.html`, `public/images/logo.svg` и так далее. + +Нам также важно, чтобы пользователь при переходе на главную страницу сайта по адресу `/` получал по умолчанию файл `public/html/index.html`, и для этого мы пишем ниже обработчик: + +```js +// Запросы на / выдают index.html по умолчанию +app.get('/', function(_, res) { + res.sendFile(join(__dirname, 'public', 'html', 'index.html')); +}); + +``` + +Кроме того, мы хотим, чтобы пользователь мог перейти на любую страницу просто по названию в URL. Для этого мы пишем еще один обработчик: +```js +// По всем остальным запросам пытаемся найти .html файл с названием, +// которое указал пользователь +app.get('/:page', function(req, res, next) { + const page = req.params.page; + const filePath = join(__dirname, 'public', 'html', `${page}.html`); + + // Проверяем существует ли файл + fs.access(filePath, fs.constants.F_OK, (err) => { + if (err) { + // Файл не существует, передаем дальше + next(); + return; + } + // Файл существует, отправляем его + res.sendFile(filePath); + }); +}); +``` + +Если пользователь откроет страницу `/about` или `/status`, то автоматически получит файл `.../backend/src/html/about.html` или `...backend/src/html/status.html`, и это будет работать и для новых страниц в этой папке. Обратите внимание, что мы также зайдействуем переменную с функцией `next`: если запрашиваемая страница не найдется, мы предположим, что дальше в скрипте сервера найдется какой-то другой обработчик, который сможет справиться с запросом. Выполняя `next()` мы передаем обработку в следующие обработчики, которые могут проигнорировать или принять этот запрос. + +Ниже у нас также есть обработчик, который выдает пользователю ошибку 404. Сюда мы как раз-то и попадаем, если в прошлом обработчике ничего не нашлось: + +```js +// Обработчик ошибки 404 - должен быть ПОСЛЕ всех маршрутов +// если ни один из предыдущих маршрутов не сработал - нам сюда. +// Отображает кастомную страничку об ошибке. +app.use(function(_, res) { + res.status(404).sendFile(join(__dirname, 'public', 'html', '404.html')); +}); +``` + +Мы пропустили один важный блок, назначение которого наиболее неочевидно: + +```js +// Маршрут с конфигурацией. +// Здесь javascript фронтенда может узнать, по какому адресу +// можно найти API бекенда +app.get('/config.json', (_, res) => { + res.json({ + backendUrl: BACKEND_URL, + environment: process.env.NODE_ENV || 'development', + }); +}); + +``` +Он будет использоваться в нашем JavaScript для того, чтобы узнать адрес бекенда. Дело в том, что оба приложения могут быть настроены по-разному и находиться в совершенно разных местах. Что делать, если мы писали код на компьютере, а затем нам нужно разместить приложение на купленном хостинге? Получается, что придется переписывать в каждом запросе адрес нашего бекенда. + +Чтобы этого не делать, мы создаем отдельный обработчик, в котором любой скрипт может спросить, по какому адресу расположено актуальное backend-приложение. К этому обработчику можно сделать HTTP-запрос и получить из объекта json значение `backendUrl` - ссылку на сервер бекенда, а далее использовать ее в скрипте. + +Сервер бекенда реализован по тем же принципам, что мы изучили на прошлом занятии. Используются GET, POST, PUT и DELETE маршруты для управления заметками, данные для простоты урока хранятся внутри массива `let notes = [];`. Однако есть одно новшество, которое нам потребовалось добавить в backend - настройку CORS: + +```js +// middlware CORS разрешает запросы с других доменов. Без этого браузер +// запретит фронтенду общаться с бекендом! +import cors from 'cors'; + +app.use(cors({ + origin: 'http://localhost:8080', // разрешаем только с этого домена + methods: ['GET', 'POST', 'PUT', 'DELETE'], // разрешаем только эти методы + allowedHeaders: ['Content-Type'] // разрешаем только эти заголовки +})); +``` + +Cross-Origin Resource Sharing (CORS) - это один из защитных механизмов браузера. У него одна простая задача: не дать скриптам на одном сайте получить доступ к данным пользователя на другом сайте. Допустим, что вы зашли на некоторую вредоносную страницу, где загруженный js-файл сделал запрос на сайт банка с вашими даными для списания всех средств. Без политики CORS у него бы получилось это сделать. +Браузер сначала уточняет у требуемого сайта, разрешает ли он запросы с других адресов. Если адреса первого и второго сайта совпадают, то всё в порядке. Если адреса отличаются, то второй сайт должен явно сообщить, что он разрешает запросы с любого другого сайта, либо с каких-то конкретных. Это делается во благо безопасности пользователя. +Поскольку у фронтенда и бекенда в нашем приложении отличаются порты (8080 для фронтенда и 3000 для бекенда), настройка CORS для нас всё также необходима, да и в любом случае станет таковой в будущем при внедрении сайта. +[Подробнее про CORS на Хабр](https://habr.com/ru/companies/macloud/articles/553826/) + +## 📘 **Урок 2: Запуск frontend- и backend-приложений** +### **Цель урока**: Освоить запуск отдельных серверов для frontend и backend, а также проверить их взаимодействие через специальную страницу. + +### **Краткая теория**: +Frontend-сервер отвечает за доставку статических файлов пользователю, таких как HTML-страницы и скрипты, и работает независимо от backend. Backend-сервер обрабатывает запросы к API, хранит данные и отвечает на них. Запуск приложений отдельно позволяет разрабатывать и тестировать части по отдельности, что упрощает отладку. + +### **Теория: Значение разделения приложений** +Раздельный запуск frontend и backend — это основа масштабируемой архитектуры веб-приложений. В реальной разработке frontend может быть размещен на одном сервере (например, для быстрой доставки статического контента через CDN), а backend — на другом, более мощном, для обработки сложных вычислений или баз данных. Это позволяет командам разработчиков работать параллельно: дизайнеры и frontend-специалисты фокусируются на интерфейсе, не дожидаясь backend, а backend-инженеры тестируют API независимо. Аналогия из жизни: представьте ресторан, где на кухне (backend) готовят блюда, а в зале (frontend), официанты подают блюда клиентам. Если кухня не работает, официанты все еще могут показать меню, но без еды опыт будет неполным. +![](memes/fish.png) + +Такая структура предотвращает "монолитные" проблемы, когда сбой в одной части останавливает все приложение. В нашем случае frontend запускается на порту 8080, а backend — на 3000, что имитирует разные домены. Без правильного запуска взаимодействие невозможно: браузер не сможет получить данные с backend из-за политик безопасности. Типичная ошибка — попытка обратиться к не запущенному серверу, что приводит к ошибкам соединения. Для отладки полезно использовать инструменты вроде браузерной консоли разработчика. + +### **Практические шаги**: +1. Перейдите в папку `frontend/src` и запустите сервер с помощью команды в VSCode/Git Bash: + ```bash + node server.js + ``` + Сервер frontend запустится на порту 8080. + +2. Откройте в браузере адрес `http://localhost:8080/status`. Вы увидите страницу проверки статуса, где будет указано, что сервер недоступен (это нормально, так как backend еще не запущен). + +3. Теперь перейдите в папку `backend/src` и запустите сервер backend аналогично: + ```bash + node server.js + ``` + Сервер backend запустится на порту 3000. + +4. Обновите страницу `http://localhost:8080/status` в браузере. Теперь статус должен измениться на "Сервер работает!", с указанием времени ответа. Если появляется ошибка, проверьте, запущены ли оба сервера, и убедитесь, что порты не заняты другими приложениями (в этом случае перезапустите серверы). Также проблемой может быть неправильно настроенный CORS. + +### **Что должно получиться / Проверка результата**: +- В консоли IDE для frontend появится сообщение "Frontend server running on port 8080". +- В консоли IDE для backend — "Backend server running on port 3000". +- На странице `/status` статус изменится с "❌ Сервер недоступен" на "✅ Сервер работает!", с отображением времени ответа (например, "150 мс"). +- Если статус не меняется, проверьте браузерную консоль на ошибки (нажмите F12) и убедитесь, что backend запущен. + + +## 📘 **Урок 3: Создание интерфейса заметок** +### **Цель урока**: Создать HTML-страницу для управления заметками. + +### **Теория**: +HTML-формы позволяют пользователю вводить данные. Элементы `` и ` + + + + + + +
+ +
+
+ + + + + + + +``` + +2. Создайте `notes.css` в `frontend/src/public/css/`: +```css +.container { max-width: 800px; margin: 0 auto; padding: 20px; } +.form-section { background: #f5f5f5; padding: 20px; border-radius: 5px; margin-bottom: 30px; } +#note-form input, #note-form textarea { width: 100%; padding: 10px; margin: 10px 0; border: 1px solid #ddd; } +#note-form textarea { height: 100px; resize: vertical; } +button { padding: 10px 20px; margin: 5px; cursor: pointer; border: 1px solid #ccc; background: white; } +button:hover { background: #f0f0f0; } +.notes-section { margin-top: 30px; } +.note-card { border: 1px solid #ddd; padding: 15px; margin: 10px 0; border-radius: 5px; background: white; } +.note-actions { margin-top: 10px; } +.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); } +.modal-content { background: white; padding: 20px; margin: 100px auto; width: 300px; border-radius: 5px; } +``` + +### **Что должно получиться**: +- Страница по адресу `http://localhost:8080/notes` с формой и местом для списка заметок +- Простой чистый интерфейс + +## 📘 **Урок 4: Основы Fetch API и Promise** + +### **Основная концепция: Синхронный и асинхронный код** + +JavaScript по своей природе является **однопоточным** языком, что означает, что он может выполнять только одну операцию за раз. Когда JavaScript сталкивается с операциями, которые могут занять время (такие как запросы к серверу, чтение файлов или таймеры), он не может просто остановиться и ждать их завершения, потому что это заблокирует весь интерфейс. + +Для решения этой проблемы используются **асинхронные операции** и **Promise (Обещания)**. Fetch API возвращает именно Promise. + +### **Теория: Что такое Promise?** + +**Promise (Обещание)** - это объект в JavaScript, который представляет результат **асинхронной операции** (операции, которая выполняется не сразу). У Promise есть три возможных состояния: + +1. **Pending (Ожидание)**: операция еще выполняется +2. **Fulfilled (Выполнено)**: операция завершилась успешно +3. **Rejected (Отклонено)**: операция завершилась с ошибкой + +### **Как работает Fetch API?** + +Функция `fetch()` принимает URL в качестве параметра и возвращает Promise. Этот Promise "оборачивает" HTTP-запрос к серверу. Поскольку запрос к серверу может занимать время (от миллисекунд до нескольких секунд), мы не можем ждать его завершения синхронно. + +### **Методы .then() и .catch()** + +Когда fetch() возвращает Promise, мы можем использовать два метода для работы с ним: + +1. **.then(функция)** - выполняется, когда Promise переходит в состояние "выполнено" +2. **.catch(функция)** - выполняется, когда Promise переходит в состояние "отклонено" + +### **Практика: Загрузка конфигурации и списка заметок** + +```javascript +// frontend/src/public/js/notes.js + +// Глобальные переменные для хранения состояния +var backendUrl = ''; +var editingNoteId = null; +var noteToDelete = null; + +// Функция для загрузки конфигурации +function loadConfig() { + // fetch() отправляет GET-запрос на сервер по указанному URL + // Возвращает Promise, который оборачивает HTTP-запрос + fetch('/config.json') + .then(function(response) { + // Первый .then() получает Response объект + // Этот объект содержит информацию об ответе сервера + // Но тело ответа еще не прочитано + + // Метод .json() читает тело ответа и парсит его как JSON + // Он тоже возвращает Promise, потому что чтение тела - асинхронная операция + return response.json(); + }) + .then(function(config) { + // Второй .then() получает уже распарсенные JSON данные + backendUrl = config.backendUrl; + console.log('Backend URL загружен:', backendUrl); + }) + .catch(function(error) { + // .catch() выполняется если любая из предыдущих операций завершится ошибкой + // Например: сервер недоступен, JSON некорректный и т.д. + console.error('Ошибка загрузки конфигурации:', error); + backendUrl = 'http://localhost:3000'; // резервный адрес + }); +} + +// Функция для загрузки всех заметок +function loadNotes() { + // Проверяем, загружен ли URL бэкенда + if (!backendUrl) { + // Если не загружен, сначала загружаем конфигурацию + // и только потом продолжаем загрузку заметок + loadConfig(); + + // После загрузки конфигурации вызываем loadNotes снова + setTimeout(function() { + loadNotes(); + }, 100); + return; + } + + // Отправляем GET-запрос к бэкенду для получения списка заметок + fetch(backendUrl + '/notes') + .then(function(response) { + // Проверяем, успешен ли HTTP-статус ответа + if (!response.ok) { + throw new Error('Сервер вернул ошибку: ' + response.status); + } + return response.json(); + }) + .then(function(result) { + // result содержит данные в формате {success: true, data: [...]} + if (result.success) { + displayNotes(result.data); + } else { + console.error('Ошибка в ответе сервера'); + } + }) + .catch(function(error) { + // Обрабатываем ошибки сети или парсинга JSON + console.error('Ошибка загрузки заметок:', error); + document.getElementById('notes-list').innerHTML = + '

Ошибка загрузки заметок. Проверьте подключение к серверу.

'; + }); +} + +// Функция для отображения заметок +function displayNotes(notes) { + var notesList = document.getElementById('notes-list'); + notesList.innerHTML = ''; + + if (notes.length === 0) { + notesList.innerHTML = '

Заметок нет. Создайте первую заметку!

'; + return; + } + + // Проходим по массиву заметок и создаем HTML для каждой + for (var i = 0; i < notes.length; i++) { + var note = notes[i]; + var noteElement = document.createElement('div'); + noteElement.className = 'note-card'; + + // Экранируем HTML-символы для безопасности + var safeTitle = escapeHtml(note.title); + var safeText = escapeHtml(note.text); + var safeAuthor = escapeHtml(note.author); + + // Создаем дату в читаемом формате + var date = new Date(note.updatedAt); + var formattedDate = date.toLocaleDateString(); + + noteElement.innerHTML = ` +

${safeTitle}

+

${safeText}

+
+ Автор: ${safeAuthor} + Дата: ${formattedDate} +
+
+ + +
+ `; + + notesList.appendChild(noteElement); + } +} + +// Функция для экранирования HTML +function escapeHtml(text) { + var div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; +} + +// Инициализация при загрузке страницы +document.addEventListener('DOMContentLoaded', function() { + // Загружаем конфигурацию при старте + loadConfig(); + + // Загружаем заметки при старте + loadNotes(); + + // Назначаем обработчик для кнопки обновления + document.getElementById('refresh-btn').addEventListener('click', function() { + loadNotes(); + }); +}); + +// Функция для редактирования заметки (доступна глобально) +window.editNote = function(noteId) { + // Отправляем GET-запрос для получения конкретной заметки + fetch(backendUrl + '/notes/' + noteId) + .then(function(response) { + return response.json(); + }) + .then(function(result) { + if (result.success) { + var note = result.data; + + // Заполняем форму данными заметки + document.getElementById('note-id').value = note.id; + document.getElementById('title').value = note.title; + document.getElementById('text').value = note.text; + document.getElementById('author').value = note.author; + document.getElementById('form-title').textContent = 'Редактировать заметку'; + document.getElementById('submit-btn').textContent = 'Обновить'; + document.getElementById('cancel-btn').style.display = 'inline-block'; + + editingNoteId = note.id; + } + }) + .catch(function(error) { + console.error('Ошибка загрузки заметки для редактирования:', error); + }); +}; +``` + +## 📘 **Урок 5: Отправка данных на сервер (POST и PUT)** + +### **Теория: HTTP-методы для отправки данных** + +1. **POST** - используется для создания новых ресурсов +2. **PUT** - используется для обновления существующих ресурсов +3. **DELETE** - используется для удаления ресурсов (будет в уроке 6) + +### **Как отправлять данные с помощью fetch()** + +Для отправки данных нужно передать объект конфигурации вторым параметром в fetch(): + +```javascript +fetch(url, { + method: 'POST', // или 'PUT', 'DELETE' + headers: { + 'Content-Type': 'application/json' // говорим серверу, что отправляем JSON + }, + body: JSON.stringify(data) // преобразуем объект в JSON-строку +}) +``` + +### **Практика: Создание и обновление заметок** + +```javascript +// Добавьте этот код в тот же файл notes.js + +// Обработка отправки формы +document.getElementById('note-form').addEventListener('submit', function(e) { + // Отменяем стандартное поведение формы (перезагрузку страницы) + e.preventDefault(); + + // Собираем данные из формы + var noteData = { + title: document.getElementById('title').value, + text: document.getElementById('text').value, + author: document.getElementById('author').value + }; + + // Определяем URL и метод в зависимости от того, редактируем мы или создаем + var url; + var method; + + if (editingNoteId) { + // Редактирование существующей заметки + url = backendUrl + '/notes/' + editingNoteId; + method = 'PUT'; + } else { + // Создание новой заметки + url = backendUrl + '/notes'; + method = 'POST'; + } + + // Отправляем запрос на сервер + fetch(url, { + method: method, + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(noteData) + }) + .then(function(response) { + return response.json(); + }) + .then(function(result) { + if (result.success) { + // Сбрасываем форму + resetForm(); + // Загружаем обновленный список заметок + loadNotes(); + } else { + console.error('Сервер вернул ошибку:', result.message); + } + }) + .catch(function(error) { + console.error('Ошибка отправки данных:', error); + }); +}); + +// Функция для сброса формы +function resetForm() { + document.getElementById('note-form').reset(); + document.getElementById('note-id').value = ''; + document.getElementById('form-title').textContent = 'Новая заметка'; + document.getElementById('submit-btn').textContent = 'Сохранить'; + document.getElementById('cancel-btn').style.display = 'none'; + editingNoteId = null; +} + +// Обработчик для кнопки отмены +document.getElementById('cancel-btn').addEventListener('click', function() { + resetForm(); +}); +``` + +## 📘 **Урок 6: Удаление данных и обработка ошибок** + +### **Теория: Модальные окна и подтверждение действий** + +Удаление данных - критическая операция, поэтому хорошей практикой является запрос подтверждения у пользователя. Мы реализуем это через модальное окно. + +### **Практика: Удаление заметок с подтверждением** + +```javascript +// Добавьте этот код в файл notes.js + +// Функция для показа модального окна удаления +window.showDeleteModal = function(noteId, noteTitle) { + noteToDelete = noteId; + + // Показываем пользователю, какую заметку он собирается удалить + document.getElementById('note-to-delete').textContent = noteTitle; + document.getElementById('delete-modal').style.display = 'block'; +}; + +// Обработчик для кнопки подтверждения удаления +document.getElementById('confirm-delete').addEventListener('click', function() { + if (!noteToDelete) { + return; + } + + // Отправляем DELETE-запрос + fetch(backendUrl + '/notes/' + noteToDelete, { + method: 'DELETE' + }) + .then(function(response) { + return response.json(); + }) + .then(function(result) { + if (result.success) { + // Перезагружаем список заметок + loadNotes(); + } + }) + .catch(function(error) { + console.error('Ошибка удаления:', error); + }) + .finally(function() { + // finally() выполнится в любом случае - успех или ошибка + // Скрываем модальное окно + document.getElementById('delete-modal').style.display = 'none'; + noteToDelete = null; + }); +}); + +// Обработчик для кнопки отмены удаления +document.getElementById('cancel-delete').addEventListener('click', function() { + document.getElementById('delete-modal').style.display = 'none'; + noteToDelete = null; +}); + +// Закрытие модального окна при клике вне его +window.addEventListener('click', function(e) { + if (e.target === document.getElementById('delete-modal')) { + document.getElementById('delete-modal').style.display = 'none'; + noteToDelete = null; + } +}); +``` + +## 🎯 **Дополнительные задания (опционально)** + +1. Добавьте отображение сообщений об ошибках пользователю (вместо только console.error можно использовать alert() либо popup окна с помощью html и css); +2. Добавьте возможность отметки заметки как "важной". В бекенде добавьте для модели заметки поле `important: true/false` и возможность редактирования этого поля через существующий маршурт PUT. На фронтенде добавьте какой-либо индикатор на заметку (кнопку с сердечком) которая отправляет запрос на редактирование статуса important, а также окрашивается в зависимости от статуса заметки. + +## 📊 **Схема работы Fetch API** + +``` +Шаг 1: Вызов fetch(url) + | + ↓ +Шаг 2: JavaScript создает Promise и начинает HTTP-запрос + │ + ├─ Если запрос успешен → Promise переходит в состояние Fulfilled + │ (выполняется .then(response => response.json())) + │ + └─ Если ошибка сети → Promise переходит в состояние Rejected + (выполняется .catch(error => ...)) + │ + ↓ +Шаг 3: Чтение тела ответа (асинхронно) + │ + ├─ Если JSON валиден → следующий .then(data => ...) + │ + └─ Если JSON невалиден → .catch(error => ...) +``` + +## 🔍 **Что важно запомнить:** + +1. **fetch() всегда возвращает Promise** - объект, представляющий будущий результат +2. **.then()** обрабатывает успешное выполнение, можно цепочкой вызывать несколько .then() +3. **.catch()** обрабатывает ошибки на любом этапе цепочки +4. **response.json()** тоже возвращает Promise, потому что чтение ответа - асинхронно +5. **finally()** выполняется в любом случае - и при успехе, и при ошибке + +## ✅ **Тестирование работы:** + +1. Запустите серверы frontend и backend с помощью команды `node` +2. Откройте в браузере: `http://localhost:8080/notes` +3. Проверьте в консоли браузера (F12 → Console), что загрузился backend URL +4. Создайте заметку через форму +5. Нажмите "Обновить список" - заметка должна появиться +6. Попробуйте отредактировать заметку +7. Проверьте удаление с подтверждением diff --git a/memes/fish.png b/memes/fish.png new file mode 100644 index 0000000..01d8283 Binary files /dev/null and b/memes/fish.png differ