JavaScript, React и Node.js: полный курс веб-разработки

Курс охватывает полный путь от основ JavaScript до создания клиентских приложений на React и серверной разработки на Node.js. Вы изучите ключевые инструменты экосистемы, работу с API, базами данных и развертывание приложений.

1. JavaScript: основы языка и современный синтаксис (ES6+)

JavaScript: основы языка и современный синтаксис (ES6+)

JavaScript — основной язык веб-разработки: он работает в браузере (интерфейсы, события, сеть) и на сервере через Node.js (API, фоновые задачи, инструменты). В этом курсе мы будем использовать JavaScript как фундамент для React (клиент) и Node.js (сервер), поэтому важно уверенно понимать базовый синтаксис и ключевые возможности современного стандарта ES6+.

Полезные справочники, к которым стоит привыкнуть обращаться:

  • MDN Web Docs: JavaScript
  • MDN: Руководство по JavaScript
  • ECMAScript на сайте TC39
  • Как запускать JavaScript

    Основные способы:

  • В браузере: DevTools Console (удобно для экспериментов).
  • В Node.js: команда node file.js или интерактивный режим node.
  • Режим строгой проверки:

  • Директива "use strict" включает более строгие правила и ошибки там, где раньше были неявные допущения.
  • В ES-модулях строгий режим включен по умолчанию.
  • Переменные: var, let, const и области видимости

    В современном JavaScript почти всегда используются let и const.

    | Ключевое слово | Можно переназначать | Область видимости | Поднимается (hoisting) | Когда использовать | |---|---:|---|---|---| | var | да | функция | да (инициализация как undefined) | по возможности избегать | | let | да | блок | да (но действует temporal dead zone) | когда переменная меняется | | const | нет | блок | да (но действует temporal dead zone) | по умолчанию |

    Пример блочной области видимости:

    Важный нюанс: const запрещает переназначение переменной, но не делает объект неизменяемым.

    Про temporal dead zone (TDZ): переменная let или const считается существующей в блоке, но до строки объявления к ней нельзя обращаться.

    Справка:

  • MDN: let
  • MDN: const
  • Типы данных и преобразования

    В JavaScript есть примитивы и объекты.

    Примитивные типы:

  • number (включая NaN, Infinity)
  • string
  • boolean
  • null
  • undefined
  • bigint
  • symbol
  • Объекты:

  • обычные объекты, массивы, функции, даты, коллекции Map и Set и т.д.
  • Проверка типа:

    Приведение к логическому значению (часто важно в условиях):

  • falsy: false, 0, "", null, undefined, NaN
  • остальные значения обычно truthy
  • == против ===

  • === — строгое сравнение без приведения типов.
  • == — сравнение с неявными преобразованиями (часто источник багов).
  • Рекомендация: использовать === и !==, а == — только если вы уверенно понимаете правила приведения.

    Справка:

  • MDN: Equality comparisons and sameness
  • Объекты, массивы и семантика ссылок

    Примитивы копируются по значению, а объекты — по ссылке.

    Копирование объекта:

  • поверхностная копия: { ...obj }, Object.assign({}, obj)
  • глубокая копия (когда нужно): structuredClone(obj) (если доступно в окружении)
  • !Иллюстрация различия копирования примитивов и передачи объектов по ссылке

    Функции: объявления, выражения и стрелочные функции

    Функции в JavaScript — значения: их можно присваивать переменным, передавать в аргументы и возвращать.

    Function Declaration и Function Expression

    Отличие, полезное на практике: объявления функций поднимаются целиком, и их можно вызывать до места объявления в коде (хотя стиль обычно рекомендует не злоупотреблять этим).

    Стрелочные функции

    Важные свойства стрелочных функций:

  • у них нет собственного this (они берут this из внешнего контекста)
  • у них нет собственного arguments (используйте rest-параметры)
  • Rest-параметры:

    Деструктуризация и оператор ... (spread/rest)

    Деструктуризация помогает компактно извлекать данные из объектов и массивов.

    Значения по умолчанию:

    ... работает в двух режимах:

  • spread: раскрыть массив или объект
  • rest: собрать оставшиеся элементы
  • Шаблонные строки

    Шаблонные строки (template literals) удобны для интерполяции и многострочных строк.

    На практике в React и Node.js классы встречаются реже, чем функции и композиция, но синтаксис важно понимать: вы будете встречать его в библиотеках и существующих кодовых базах.

    Справка:

  • MDN: Classes
  • Асинхронность: Promise и async/await

    JavaScript часто работает с задачами, которые завершаются позже:

  • запросы по сети
  • чтение файлов (в Node.js)
  • таймеры
  • Promise

    Promise — объект, который представляет результат асинхронной операции: будущий успешный результат или ошибку.

    Справка:

  • MDN: Promise
  • async/await

    async/await — синтаксический сахар над Promise, который делает асинхронный код похожим на синхронный.

    Параллельный запуск нескольких операций:

    !Схема, объясняющая почему обработчики Promise выполняются раньше setTimeout при прочих равных

    Полезные современные возможности ES2020+

    Опциональная цепочка ?. — безопасный доступ к вложенным полям:

    Оператор объединения с nullish-значением ?? — значение по умолчанию только для null и undefined:

    Отличие от ||: || считает 0 и пустую строку ложными и заменит их значением по умолчанию, что не всегда нужно.

    Справка:

  • MDN: Optional chaining
  • MDN: Nullish coalescing
  • Встроенные методы массивов, Map и Set

    Методы массивов помогают писать декларативный код:

  • map — преобразование элементов
  • filter — отбор по условию
  • reduce — свертка к одному значению
  • find — поиск первого подходящего
  • Map удобен для словаря с любыми ключами, Set — для множества уникальных значений.

    Справка:

  • MDN: Map
  • MDN: Set
  • Практические рекомендации перед React и Node.js

  • Используйте const по умолчанию, let — когда нужно переназначение.
  • Старайтесь писать функции без побочных эффектов там, где это естественно: это упрощает тестирование и работу с состоянием в React.
  • Явно обрабатывайте ошибки в асинхронном коде: try/catch с await или .catch у Promise.
  • Привыкайте читать MDN как основной справочник.
  • В следующей части курса обычно логично перейти к работе с DOM, событиями и браузерным API, а затем — к инструментам разработки и основам React.

    2. Асинхронность, работа с API и управление состоянием

    Асинхронность, работа с API и управление состоянием

    Асинхронность в JavaScript нужна почти в каждой реальной задаче: загрузить данные по сети, прочитать файл, подождать таймер, обработать событие пользователя. В предыдущей статье мы познакомились с Promise и async/await. Теперь соберём это в практическую систему: как правильно ходить в API, обрабатывать ошибки и отмену запросов, а также как хранить и обновлять состояние приложения так, чтобы UI не «ломался».

    Ключевая идея: асинхронность почти всегда означает изменение состояния во времени. Поэтому темы API и управление состоянием неразрывно связаны.

    Ментальная модель: как выполняется асинхронный код

    JavaScript выполняет код в одном потоке: в каждый момент времени выполняется одна инструкция. Асинхронность достигается не параллельным выполнением JavaScript-кода, а очередями задач и колбэков.

    Основные элементы модели выполнения:

  • Call Stack (стек вызовов): какие функции выполняются прямо сейчас.
  • Task Queue (очередь задач): например, колбэки setTimeout, события DOM.
  • Microtask Queue (очередь микрозадач): продолжения промисов (.then, catch, finally) и queueMicrotask.
  • Почти всегда микрозадачи выполняются раньше, чем следующая обычная задача.

    !Схема очередей задач и микрозадач и их приоритета

    Пример, который часто удивляет:

    Обычно порядок будет: A, D, C, B.

    Почему так:

  • A и D печатаются сразу (синхронный код).
  • .then(...) попадает в очередь микрозадач.
  • setTimeout попадает в очередь задач.
  • После завершения текущего синхронного кода движок сначала выполняет микрозадачи, потом берёт задачи.
  • Полезная ссылка:

  • MDN: Concurrency model and the event loop
  • Что такое API и как обычно устроен обмен данными

    API (Application Programming Interface) в веб-контексте чаще всего означает HTTP-сервис, который принимает запрос и возвращает ответ.

    Типовой сценарий:

  • Клиент отправляет HTTP-запрос (например, GET /users/42).
  • Сервер возвращает ответ со статусом (например, 200 OK или 404 Not Found) и телом.
  • Часто тело ответа — JSON.
  • Главное для фронтенда: сеть ненадёжна. Запрос может быть медленным, может завершиться ошибкой, может вернуться не тот ответ, который «актуален» (гонки), запрос может понадобиться отменить.

    fetch: базовая работа с HTTP в браузере и Node.js

    Современный стандартный способ запросов — fetch.

  • В браузере fetch доступен по умолчанию.
  • В Node.js fetch встроен, начиная с Node 18.
  • Ссылки:

  • MDN: Fetch API
  • Node.js: fetch
  • Простой GET и разбор JSON

    Что важно запомнить:

  • fetch(...) возвращает Promise<Response>.
  • res.ok — быстрый способ проверить, что статус в диапазоне 200–299.
  • Разбор тела (res.json(), res.text()) тоже асинхронный.
  • Ссылка:

  • MDN: Response.ok
  • POST с JSON-телом

    Почему важен finally: даже если запрос завершился ошибкой, таймер надо убрать, иначе можно получить лишние срабатывания и утечки ресурсов.

    Параллельные запросы и стратегии ожидания

    Частая задача: загрузить несколько ресурсов.

    Promise.all: «всё или ничего»

    Особенности:

  • Если любой промис отклонится, Promise.all сразу упадёт в catch.
  • Подходит, когда без каждого элемента нельзя продолжать.
  • Promise.allSettled: собрать результаты, даже если часть упала

    Подходит, когда вы хотите показать пользователю частичный результат.

    Гонки (race conditions): как «старый» ответ ломает UI

    Классический пример: пользователь быстро вводит текст, вы отправляете запрос на подсказки при каждом вводе.

    Проблема:

  • Запрос 1 (старый) может ответить позже запроса 2 (нового).
  • Если без защиты просто писать в состояние, UI может показать устаревшие данные.
  • Два базовых решения:

  • Отменять предыдущий запрос через AbortController.
  • Хранить requestId и применять результат только если он «последний».
  • Пример с requestId:

    Управление состоянием: что это и почему без него сложно

    Состояние — это данные, которые:

  • могут меняться со временем
  • влияют на то, что пользователь видит (UI)
  • Примеры состояния:

  • текущий пользователь
  • список товаров
  • текст в поле ввода
  • флаги isLoading, error
  • Самая частая ошибка новичков: смешать данные запроса и логику их жизненного цикла так, что приложение начинает вести себя непредсказуемо.

    Два типа состояния: UI state и server state

  • UI state: локальное состояние интерфейса (открыт ли модал, какая вкладка активна, что введено в инпут).
  • Server state: данные, которые приходят с сервера (профиль пользователя, список постов). У них есть дополнительные свойства: кэширование, протухание, повторные запросы, синхронизация.
  • В React (к которому мы придём дальше) эти типы состояния часто управляются разными подходами: локальное состояние компонентами, а server state — библиотеками кэширования или явной логикой загрузки.

    Ссылка для дальнейшего чтения (когда перейдёте к React):

  • React: State — memory of a component
  • Шаблон для асинхронного состояния: idle/loading/success/error

    Практичный минимальный автомат состояний для загрузки данных:

  • idle — ещё не загружали
  • loading — запрос в процессе
  • success — данные получены
  • error — произошла ошибка
  • !Диаграмма жизненного цикла загрузки данных

    Пример на чистом JavaScript (подготовка к useReducer в React):

    Ключевой принцип: обновление состояния должно быть предсказуемым.

    Почему важна неизменяемость (immutability)

    Когда вы обновляете состояние, полезно создавать новый объект, а не менять старый. Это:

  • упрощает отладку (видно, когда и что изменилось)
  • помогает UI-фреймворкам эффективно понимать, что нужно перерисовать
  • Вместо:

    Лучше:

    То же самое работает для массивов:

  • добавить: [...arr, item]
  • удалить: arr.filter(...)
  • заменить: arr.map(...)
  • Мини-кэш для запросов: не ходить в сеть лишний раз

    Даже без библиотек можно сделать простой кэш на Map.

    Почему кэшировать можно именно промис:

  • если два места в приложении одновременно попросили один ресурс, они получат один и тот же промис
  • это снижает нагрузку на сеть и сервер
  • Практические рекомендации перед переходом к React и Node.js

  • Всегда разделяйте: получение данных и обновление состояния.
  • Проверяйте res.ok, не полагайтесь на то, что fetch упадёт на 404.
  • Для UI почти всегда нужен минимум из трёх флагов: loading, data, error (или один status).
  • Защищайтесь от гонок: отменой (AbortController) или проверкой актуальности ответа.
  • Кэшируйте и избегайте дублирующих запросов, особенно при ререндерингах (в React это критично).
  • Дальше, когда мы перейдём к React, вы увидите те же идеи в виде хуков (useState, useEffect, useReducer) и более продвинутых стратегий для server state.

    3. React: компоненты, хуки, роутинг и архитектура приложения

    React: компоненты, хуки, роутинг и архитектура приложения

    React — библиотека для построения пользовательских интерфейсов. Она помогает описывать UI как функцию от состояния: какие данные есть сейчас {variant}} onClick={onClick}> {children} </button> ); }

    function App() { return ( <Button variant="danger" onClick={() => console.log("click")}> Удалить </Button> ); } js function Status({ isLoading, error }) { if (isLoading) return <p>Загрузка...</p>; if (error) return <p>Ошибка: {error.message}</p>; return <p>Готово</p>; } js function TodoList({ todos }) { return ( <ul> {todos.map((t) => ( <li key={t.id}>{t.title}</li> ))} </ul> ); } js import { useState } from "react";

    function Counter() { const [count, setCount] = useState(0);

    return ( <div> <p>Счёт: {count}</p> <button onClick={() => setCount(count + 1)}>+1</button> </div> ); } js setItems((prev) => [...prev, newItem]); js import { useState } from "react";

    function SearchBox({ onSubmit }) { const [query, setQuery] = useState("");

    return ( <form onSubmit={(e) => { e.preventDefault(); onSubmit(query); }} > <input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Введите запрос" /> <button type="submit">Искать</button> </form> ); } js import { useEffect, useState } from "react";

    function UserCard({ userId }) { const [status, setStatus] = useState("idle"); const [user, setUser] = useState(null); const [error, setError] = useState(null);

    useEffect(() => { if (!userId) return;

    const controller = new AbortController();

    async function run() { setStatus("loading"); setError(null); setUser(null);

    try { const res = await fetch(https://api.example.com/users/{res.status});

    const data = await res.json(); setUser(data); setStatus("success"); } catch (e) { if (e.name === "AbortError") return; setError(e); setStatus("error"); } }

    run();

    return () => { controller.abort(); }; }, [userId]);

    if (status === "idle") return <p>Выберите пользователя</p>; if (status === "loading") return <p>Загрузка...</p>; if (status === "error") return <p>Ошибка: {error.message}</p>;

    return <div>Имя: {user.name}</div>; } js import { useCallback, useMemo, useState } from "react";

    function Products({ items }) { const [query, setQuery] = useState("");

    const filtered = useMemo(() => { const q = query.trim().toLowerCase(); return items.filter((x) => x.title.toLowerCase().includes(q)); }, [items, query]);

    const onChange = useCallback((e) => { setQuery(e.target.value); }, []);

    return ( <div> <input value={query} onChange={onChange} /> <ul> {filtered.map((x) => ( <li key={x.id}>{x.title}</li> ))} </ul> </div> ); } js import { useReducer } from "react";

    const initialState = { status: "idle", data: null, error: null };

    function reducer(state, action) { switch (action.type) { case "start": return { status: "loading", data: null, error: null }; case "success": return { status: "success", data: action.data, error: null }; case "error": return { status: "error", data: null, error: action.error }; default: return state; } }

    function Example() { const [state, dispatch] = useReducer(reducer, initialState); return ( <div> <p>Статус: {state.status}</p> <button onClick={() => dispatch({ type: "start" })}>Start</button> </div> ); } js import { BrowserRouter, Link, Route, Routes, useParams } from "react-router-dom";

    function Home() { return <h1>Главная</h1>; }

    function UserPage() { const { id } = useParams(); return <h1>Пользователь {id}</h1>; }

    export default function App() { return ( <BrowserRouter> <nav> <Link to="/">Главная</Link> <Link to="/users/42">User 42</Link> </nav>

    <Routes> <Route path="/" element={<Home />} /> <Route path="/users/:id" element={<UserPage />} /> </Routes> </BrowserRouter> ); } js // api/users.js export async function getUser(id, { signal } = {}) { const res = await fetch(/api/users/{res.status}); return await res.json(); } txt src/ app/ App.jsx router.jsx shared/ ui/ Button.jsx lib/ fetchJson.js features/ auth/ api/ login.js model/ useAuth.js ui/ LoginForm.jsx pages/ home/ HomePage.jsx user/ UserPage.jsx `

    Идея:

  • shared — переиспользуемые элементы без привязки к бизнес-смыслу;
  • features — законченные функции продукта (авторизация, корзина);
  • pages — страницы для роутера.
  • Выбор стратегии управления состоянием

    В реальных проектах обычно комбинируют несколько подходов.

  • Локально в компоненте: useState.
  • В дереве компонентов: поднятие состояния вверх и проброс через props.
  • Глобально в приложении: Context для редких общих данных.
  • Сложные сценарии: useReducer или внешнее хранилище.
  • Полезные ссылки для расширения:

  • React: Context
  • Redux Toolkit
  • TanStack Query
  • Практический ориентир:

  • Если проблема — “пробрасываем слишком много пропов” и данные действительно глобальные, смотрите в сторону Context.
  • Если проблема — “сложно синхронизировать серверные данные, кэш, рефетч”, смотрите в сторону TanStack Query.
  • Границы ошибок

    Ошибка в рендере компонента может “уронить” всё дерево ниже. Для изоляции используют Error Boundary.

    Ссылка:

  • React: Error Boundaries
  • Даже если вы пока не пишете Error Boundary сами, важно знать концепцию: в продакшене ошибки рендера должны приводить к понятному экрану, а не к пустой странице.

    Частые ошибки и как их избегать

  • Мутация объектов и массивов в состоянии вместо создания новых значений.
  • Нестабильные key в списках и странные баги при вставке/удалении элементов.
  • Запросы в useEffect без отмены: гонки и обновление состояния после размонтирования.
  • Неправильные зависимости useEffect: устаревшие значения или бесконечные циклы.
  • Использование useMemo и useCallback “на всякий случай” вместо понимания узкого места.
  • Связь с дальнейшими темами курса

    React закрывает клиентскую часть: компоненты, состояние, роутинг, загрузку данных. Следующий логичный шаг в полном стеке — Node.js сервер:

  • реализовать реальные эндпоинты вместо https://api.example.com/...`;
  • добавить авторизацию;
  • научиться проектировать контракт API, чтобы фронтенд и бэкенд развивались согласованно.
  • 4. Node.js: сервер, REST API, аутентификация и базы данных

    Node.js: сервер, REST API, аутентификация и базы данных

    Node.js позволяет запускать JavaScript на сервере. В контексте нашего курса это следующий логичный шаг после React: теперь мы не будем обращаться к https://api.example.com/..., а построим свой сервер с понятным контрактом API, аутентификацией и хранением данных.

    Связка выглядит так:

  • React отвечает за UI и отправляет запросы через fetch.
  • Node.js отвечает за маршруты API, валидацию, безопасность.
  • База данных хранит пользователей и доменные данные.
  • Полезные источники:

  • Документация Node.js
  • Документация Express
  • MDN: HTTP
  • bcrypt
  • jsonwebtoken
  • Prisma
  • Документация PostgreSQL
  • OWASP Cheat Sheet Series
  • Что такое сервер в веб-разработке

    Сервер в типичном веб-приложении:

  • принимает HTTP-запросы
  • проверяет права доступа
  • читает и пишет данные в базу
  • возвращает HTTP-ответы
  • Важно: HTTP по природе stateless, то есть каждый запрос сам по себе не помнит контекст прошлого запроса. Аутентификация и сессии решают эту проблему прикладным способом.

    !Общая схема пути запроса от React до базы данных и обратно

    Подготовка проекта Node.js

    Базовые шаги:

  • установить Node.js (желательно актуальную LTS-версию)
  • создать проект и поставить зависимости
  • настроить переменные окружения
  • Пример:

    Минимальный package.json для разработки:

    Переменные окружения (файл .env):

    Пояснения:

  • PORT порт, на котором слушает сервер
  • JWT_SECRET секрет для подписи токенов
  • DATABASE_URL строка подключения к базе данных
  • Express: маршруты и middleware

    Express это популярный минималистичный фреймворк для HTTP-серверов.

    Два ключевых понятия:

  • роут (route) сопоставляет URL и HTTP-метод с обработчиком
  • middleware это функция, которая выполняется между получением запроса и формированием ответа
  • Middleware обычно используют для:

  • парсинга JSON-тела (express.json())
  • логирования
  • CORS
  • проверки аутентификации
  • централизованной обработки ошибок
  • Минимальный сервер:

    В реальных проектах часто используют библиотеки схем, чтобы не писать проверки руками. Например, Zod.

    Аутентификация: сессии и JWT

    Аутентификация отвечает на вопрос: кто ты?.

    Авторизация отвечает на вопрос: что тебе можно?.

    Два распространённых подхода:

  • сессии на сервере и cookie с идентификатором сессии
  • JWT-токены, которые сервер может проверять без хранения сессии
  • Пароли: хранение только в виде хеша

    Пароль нельзя хранить в базе как есть. Правильный подход:

  • вычислить хеш пароля
  • использовать salt (случайную добавку), чтобы одинаковые пароли не давали одинаковые хеши
  • при входе сравнивать хеши
  • Популярная библиотека: bcrypt.

    Число 10 это параметр стоимости, который влияет на время вычисления.

    JWT: идея и типичный поток

    JWT это строка, подписанная сервером. Внутри обычно есть полезная нагрузка (например, userId) и срок действия.

    Библиотека: jsonwebtoken.

    Где хранить токен на клиенте:

  • часто выбирают cookie с флагом HttpOnly, чтобы токен был недоступен из JavaScript
  • вариант с localStorage проще, но повышает риски при XSS
  • !Как работает вход и доступ к защищённым маршрутам через JWT

    Middleware для защиты маршрутов

    Этот пример использует заголовок Authorization: Bearer .... Если вы выберете cookie-подход, токен будет браться из req.cookies (для этого нужен cookie-parser).

    База данных: зачем, какая и как подключать

    Без базы данных сервер обычно превращается в набор временных заглушек.

    Два популярных типа:

  • реляционные базы (например, PostgreSQL): таблицы, связи, строгая схема
  • документные базы (например, MongoDB): JSON-подобные документы, более гибкая структура
  • Для классических приложений с пользователями, правами, связями и отчётами PostgreSQL часто оказывается наиболее предсказуемым выбором.

    ORM и миграции

    ORM это слой, который помогает работать с базой через объекты и методы, а не через ручные SQL-запросы.

    Миграции это история изменений схемы базы данных:

  • добавили таблицу
  • добавили колонку
  • изменили индекс
  • Одна из популярных связок для Node.js и PostgreSQL: Prisma.

    Ссылка: Prisma: Getting Started

    Пример модели данных (Prisma)

    schema.prisma:

    Подключение клиента Prisma:

    CRUD-эндпоинт с базой данных

    Пример: получить список постов.

    И подключение роутера:

    Интеграция с React: контракт и типичные проблемы

    CORS и credentials

    Если фронтенд и бэкенд на разных доменах или портах, браузер применяет политику CORS.

    Типичные симптомы:

  • запрос виден в DevTools, но заблокирован
  • ошибка про CORS policy
  • Минимальные правила:

  • на сервере разрешить origin фронтенда
  • если используются cookie, включить credentials: true на сервере и credentials: "include" на клиенте
  • Пример запроса из React:

    Справка: MDN: CORS

    Единая обработка res.ok

    Как мы обсуждали в теме про API: fetch не падает автоматически на 404 или 500. Поэтому удобно иметь общий helper на фронтенде, и симметрично на сервере иметь единый формат ошибок.

    Безопасность: минимальный практический набор

    Это не полный курс по security, но базовый минимум важен сразу.

  • не хранить пароли в открытом виде, использовать bcrypt
  • не отправлять подробности внутренних ошибок клиенту
  • проверять входные данные
  • ограничивать CORS точными origin, а не *
  • не складывать секреты в репозиторий, использовать переменные окружения
  • Полезное чтение: OWASP Cheat Sheet Series

    Рекомендуемая структура проекта

    Один из практичных вариантов, чтобы код не превратился в монолит:

    Идея такая:

  • routes только разбирают HTTP и вызывают бизнес-логику
  • services содержат правила предметной области
  • middleware отвечает за сквозные вещи
  • db инкапсулирует доступ к данным
  • Как это связывается с темами курса

  • Из JavaScript-основ вы используете модули, async/await, работу с объектами.
  • Из темы про асинхронность вы приносите дисциплину обработки ошибок и понимание жизненного цикла данных.
  • Из React вы приносите требования UI: стабильные контракты, предсказуемые статусы, защита от гонок и понятный формат ошибок.
  • Следующий шаг в полном стеке обычно включает:

  • более полноценную аутентификацию (refresh-токены, ротация)
  • роли и права (authorization)
  • тестирование API
  • деплой и конфигурацию окружений
  • 5. Тестирование, безопасность, производительность и деплой

    Тестирование, безопасность, производительность и деплой

    Когда вы уже умеете писать клиент на React и сервер на Node.js, следующий шаг до уровня продакшн-разработки — научиться:

  • проверять код автоматически (тестирование)
  • защищать приложение (безопасность)
  • делать его быстрым и устойчивым (производительность)
  • выкатывать изменения так, чтобы они повторяемо собирались и запускались (деплой)
  • Эта тема связывает все предыдущие части курса:

  • из JavaScript вы берёте дисциплину работы с модулями, асинхронностью и чистыми функциями
  • из React — предсказуемое состояние и компонентный подход
  • из Node.js — контракт API, middleware, аутентификацию и работу с базой данных
  • !Конвейер CI/CD и место тестов в жизненном цикле релиза

    Тестирование: что именно мы проверяем

    Тесты отвечают на вопрос: как быстро и уверенно мы можем менять код, не ломая продукт.

    Полезные источники:

  • Документация Vitest
  • Документация Jest
  • Testing Library
  • Документация Playwright
  • SuperTest (репозиторий)
  • Виды тестов и «пирамида тестирования»

    Практичная модель — держать много быстрых тестов и мало дорогих.

    | Вид теста | Что проверяет | Скорость | Типичный инструмент | |---|---|---|---| | Unit | функция или модуль изолированно | высокая | Vitest, Jest | | Integration | взаимодействие модулей (например, роут + БД) | средняя | Vitest/Jest + SuperTest | | E2E | поведение приложения целиком через браузер | низкая | Playwright |

    Ключевой принцип: чем ближе тест к реальному пользователю, тем он дороже и хрупче, но тем больше уверенности даёт.

    Unit-тесты: чистые функции и бизнес-правила

    Лучше всего тестируются функции без побочных эффектов: дали входные данные — получили результат.

    Пример на Vitest.

    Тестирование React: проверяем поведение, а не реализацию

    Для компонентов важнее всего проверять пользовательские сценарии:

  • что отрисовано
  • что происходит при клике
  • как меняется UI при loading/error/success
  • Подход Testing Library: искать элементы так, как это делает пользователь (по тексту, роли, label).

    Практика: если компонент сильно зависит от запросов, вынесите работу с API в отдельные функции или кастомные хуки, чтобы тесты могли подменять зависимости.

    Интеграционные тесты Node.js API: роуты, middleware и формат ошибок

    Для API критично тестировать:

  • статусы (200/201/400/401/403/404/500)
  • единый формат ошибок
  • аутентификацию на защищённых маршрутах
  • Пример идеи с SuperTest (тестируем Express-приложение как HTTP-сервер).

    Если вы используете базу данных, обычно применяют один из подходов:

  • тестовая база + миграции перед прогоном
  • транзакции и откат после теста
  • контейнер БД в CI
  • E2E-тесты: критические пользовательские пути

    E2E проверяют «сквозные» сценарии:

  • логин
  • просмотр списка
  • создание сущности
  • доступ к защищённой странице
  • E2E не должны быть слишком многочисленными. Обычно тестируют только главные деньги продукта.

    Playwright удобен тем, что поддерживает современные браузеры и параллельный запуск.

    Безопасность: минимальный обязательный набор

    Цель безопасности — снизить вероятность и ущерб от атак, не делая систему чрезмерно сложной.

    Полезные источники:

  • OWASP Top 10
  • OWASP Cheat Sheet Series
  • helmet (npm)
  • express-rate-limit (npm)
  • Zod (репозиторий)
  • Главное правило: доверяйте только серверу

    Любые проверки на фронтенде — это UX. Безопасность и корректность — это сервер.

    Что это означает на практике:

  • валидировать входные данные на сервере
  • проверять права доступа на сервере
  • не раскрывать внутренние ошибки клиенту
  • Типовые угрозы в full-stack приложении

  • Injection: опасные запросы в БД или интерпретатор (лечится параметризацией и ORM)
  • XSS: внедрение скриптов в страницу (лечится экранированием, CSP, запретом опасного HTML)
  • CSRF: подделка запросов от имени пользователя при cookie-аутентификации (лечится CSRF-токенами и настройкой cookie)
  • Broken Auth: ошибки в JWT, паролях, хранении токенов
  • Misconfiguration: утечки секретов, слишком широкий CORS, режим debug в продакшне
  • Базовые HTTP-заголовки и защита Express

    helmet включает набор защитных заголовков.

    Ограничение частоты запросов полезно против брутфорса на логине.

    Валидация входных данных: схема вместо ручных if

    Схемы уменьшают количество ошибок и упрощают поддержку.

    Важно: даже при схемах ошибки нужно приводить к вашему единому формату ответа.

    JWT, cookie и хранение токенов

    Если вы храните JWT на клиенте, выбирайте осознанно:

  • localStorage проще, но опаснее при XSS
  • cookie с HttpOnly защищает от чтения токена из JavaScript, но требует аккуратной настройки против CSRF
  • Если вы используете cookie, обратите внимание на параметры:

  • HttpOnly
  • Secure (включать в HTTPS)
  • SameSite (часто Lax или Strict, зависит от сценариев)
  • CORS: разрешайте только то, что нужно

    Неправильно:

  • разрешать origin: "*" для приватного API
  • Правильно:

  • явно указывать origin фронтенда
  • включать credentials только если реально нужны cookie
  • Производительность: быстрый UI и устойчивый сервер

    Производительность — это не только «быстрее», но и «стабильно под нагрузкой».

    Полезные источники:

  • React: Optimizing Performance
  • Документация Node.js
  • web.dev
  • !Карта типичных узких мест производительности в full-stack приложении

    Производительность React

    Главные практики:

  • не делать лишние рендеры из-за мутаций и нестабильных ссылок
  • разделять server state и UI state, чтобы не перерисовывать всё подряд
  • код-сплиттинг страниц и тяжёлых компонентов
  • Ленивая загрузка маршрута или тяжёлого компонента:

    Точка контроля: сначала добейтесь корректности, затем измеряйте (React DevTools Profiler, Performance вкладка браузера) и оптимизируйте по факту.

    Производительность Node.js API

    Главные источники проблем:

  • слишком много запросов к БД
  • отсутствие кэширования
  • тяжёлые вычисления в одном потоке
  • большие JSON-ответы без сжатия
  • Практики:

  • фильтровать и пагинировать данные
  • выбирать только нужные поля
  • добавлять кэш (в памяти, Redis) для часто читаемых данных
  • включать сжатие ответа
  • Сжатие (если это уместно для вашего трафика):

    Пакет:

  • compression (npm)
  • Производительность базы данных

    База часто становится узким местом раньше, чем Node.js.

    Базовые правила:

  • индексы под частые фильтры и сортировки
  • избегать N+1 запросов (когда на список делается много запросов по одному на элемент)
  • возвращать только нужные поля
  • Если вы используете ORM, не забывайте смотреть, какие реальные SQL-запросы она отправляет.

    Деплой: как доставить React и Node.js в продакшн

    Деплой — это повторяемая процедура сборки и запуска.

    Полезные источники:

  • Документация Docker
  • GitHub Actions
  • Vercel Docs
  • Netlify Docs
  • Render Docs
  • Fly.io Docs
  • Окружения и конфигурация

    Минимально полезно разделять:

  • development (локально)
  • staging (проверка перед релизом)
  • production (боевой)
  • Правила:

  • секреты хранятся в переменных окружения, а не в репозитории
  • разные базы данных для staging и production
  • логирование и мониторинг включены в production
  • Сборка фронтенда

    Типичный пайплайн:

  • установить зависимости
  • запустить тесты
  • собрать (build)
  • раздать статические файлы через CDN или веб-сервер
  • Варианты деплоя фронтенда:

  • статический хостинг (Vercel, Netlify)
  • раздача из Node.js (подходит для простых случаев, но хуже масштабируется)
  • Деплой бэкенда

    Базовые варианты:

  • платформа как сервис (Render, Fly.io)
  • VM/сервер + процесс-менеджер
  • контейнеризация Docker
  • Практический минимум для API:

  • health-check эндпоинт (например, /api/health)
  • корректная обработка ошибок и логирование
  • миграции базы данных как отдельный шаг релиза
  • CI/CD: автоматизация

    Реалистичная минимальная схема:

  • При push или pull request запускать линтер и тесты.
  • При мерже в main собирать и деплоить в staging.
  • В staging прогонять e2e.
  • По кнопке или по тегу деплоить в production.
  • Важно: деплой без тестов почти всегда приводит к накоплению ошибок и страху изменений.

    Наблюдаемость: логи и метрики

    Чтобы поддерживать систему после деплоя, нужны:

  • структурированные логи на сервере
  • отслеживание ошибок (как минимум логирование stack trace для 5xx)
  • базовые метрики: время ответа, процент ошибок, нагрузка
  • Даже без сложных систем мониторинга, дисциплина логов и единый формат ошибок сильно помогают.

    Практический чеклист перед релизом

  • все запросы к API имеют предсказуемый формат ошибок
  • есть тесты на ключевые сценарии (unit, интеграционные, несколько e2e)
  • входные данные валидируются на сервере
  • CORS настроен строго под ваши origin
  • пароли хранятся только как хеш
  • секреты не попали в репозиторий
  • фронтенд собирается в production-режиме
  • есть health-check и понятные логи
  • Эта статья завершает «полный цикл» full-stack разработки: от языка и UI до API, безопасности, скорости и выкладки. Дальше типичный путь роста — углубление в архитектуру (слои, доменная модель), наблюдаемость, масштабирование и более строгая автоматизация релизов.