воркшоп · Node.js
Веб-магазин на Node.js + Express
Собираем работающий интернет-магазин: каталог, карточка товара, корзина, вход по паролю и защищённая админка, куда пускают только по роли. Не «hello world», а настоящее веб-приложение с HTTP, MVC, JWT-аутентификацией и middleware.
Как устроен воркшоп
Это воркшоп, а не лекция. Сначала — теория, которая нужна, чтобы понимать, что мы пишем, потом — код руками. Каждый кусок ты вбиваешь в свой файл и сразу проверяешь в браузере.
| Что делаем | Что НЕ делаем |
|---|---|
| Пишем свой сервер с нуля, файл за файлом | Не клонируем готовый репозиторий и не «вайбкодим» весь проект одним промптом |
| Копируем маленькие куски (5–20 строк) и разбираем каждую строку | Не вставляем 300 строк, которые «просто работают» |
После каждого шага открываем http://localhost:3000 и проверяем | Не ждём конца урока, чтобы запустить |
| Ломаем нарочно (меняем роль, портим токен) и смотрим, что будет | Не молчим, если 401/403/500 непонятны |
Вот что получится в итоге:


Блок 0.1: Что вообще такое «сайт»
Прежде чем писать сервер, надо понять, что происходит, когда ты набираешь адрес и жмёшь Enter.
Немного истории
В 1989 году Тим Бернерс-Ли в лаборатории CERN предложил систему связанных документов — World Wide Web. В 1991 появился первый в мире сайт — просто текст со ссылками. Три изобретения, которые он сделал, живут до сих пор:
- HTML — язык разметки документа;
- HTTP — протокол, по которому документ передаётся;
- URL — адрес документа.
Что происходит, когда ты открываешь адрес
Ты в браузере — это клиент. Где-то стоит сервер — компьютер, который вечно ждёт запросы и отдаёт ответы. Между вами — интернет. Вот полный путь одного открытия страницы:
Что происходит при открытии shop.kz
0 / 7Нажми Start и пройди путь запроса по шагам.
Зачем нужен DNS?
Блок 0.2: HTML, CSS, JS — три разных материала
Браузер получает три вида файлов, и относится к ним по-разному. Это важно понять, чтобы позже осознанно решать, что рендерить на сервере, а что в браузере.
| Материал | Что это | Как браузер это грузит |
|---|---|---|
| HTML | Скелет страницы (структура, текст) | Потоково (streaming): браузер начинает рисовать, не дожидаясь конца файла. Первые теги видны сразу. |
| CSS | Внешний вид (цвета, отступы, шрифты) | Блокирует рендер: пока CSS не загружен целиком, браузер не покажет страницу — чтобы не мигать нестилизованным текстом. |
| JS | Поведение (клики, запросы, логика) | Блокирует парсинг (если не async/defer): встретив <script>, браузер останавливает разбор HTML и выполняет скрипт. |
Сайт против веб-приложения
| Сайт (website) | Веб-приложение (web app) |
|---|---|
| Показывает информацию (блог, новости, визитка) | Пользователь что-то делает: логинится, кладёт в корзину, платит |
| Каждая страница — отдельный HTML с сервера | Много логики, состояние (кто вошёл, что в корзине) |
| Читаешь и уходишь | Взаимодействуешь: формы, кнопки, обновления без перезагрузки |
Блок 0.3: HTTP и HTTPS — язык клиента и сервера
HTTP (HyperText Transfer Protocol) — это правила, по которым клиент и сервер разговаривают. Он до смешного простой: это просто текст. Запрос и ответ — обычные строки.
Запрос всегда: [метод] [путь] + заголовки + (иногда) тело.
Ответ всегда: [статус-код] + заголовки + (иногда) тело.
🔍 Анатомия HTTP-запроса
Наведи на выделенный элемент — увидишь пояснение
← Запрос (Request)
Headers
← Ответ (Response)
Headers
Body (JSON)
<!doctype html><html>...каталог...</html>Методы: 4 глагола
Статус-коды: ответ одним числом
| Код | Значит | Когда увидишь |
|---|---|---|
| 2xx (200, 201, 204) | Успех | 200 OK — страница отдана; 201 Created — создано; 204 — успех без тела |
| 3xx (301, 302) | Редирект | После логина сервер отвечает 302 → «иди на /» |
| 4xx (400, 401, 403, 404) | Ошибка клиента | 401 — ты не залогинен; 403 — залогинен, но нет прав; 404 — нет такой страницы |
| 5xx (500) | Ошибка сервера | Твой код упал с исключением |
Попробуй HTTP руками
Ниже — настоящий HTTP-клиент прямо в уроке. Нажми «Отправить запрос»: браузер сделает реальный запрос к публичному API и покажет живой ответ — статус, заголовки, тело.
HTTP вживую — потыкай методы
https://jsonplaceholder.typicode.comЗапрос
GEThttps://jsonplaceholder.typicode.com/posts/1Нажми «Отправить запрос» — браузер сделает настоящий HTTP-запрос к серверу и вернёт реальный ответ.
А что добавляет «S» в HTTPS?
Пользователь залогинен как обычный user и открывает /admin. Какой статус-код логичнее вернуть?
Блок 0.4: Что такое Node.js и зачем он
JavaScript родился в браузере (1995, Брендан Айк, за 10 дней). Долго он только там и жил. В 2009 Райан Даль взял движок V8 из Google Chrome (он превращает JS в машинный код) и приделал к нему доступ к файлам и сети. Так родился Node.js — JavaScript, который работает вне браузера, на сервере. 6
| JS в браузере | JS в Node.js |
|---|---|
Есть window, document, DOM | Нет DOM — зато есть файлы, сеть, процессы (fs, http, process) |
| Не может читать твой диск (безопасность) | Может: читает файлы, слушает порты, ходит в БД |
| Задача — рисовать страницу | Задача — быть сервером: принимать запросы, отдавать ответы |
Почему Node так устроен: одна нить + событийный цикл
Что Node.js взял из браузера Chrome?
Блок 0.5: MVC — как не утонуть в коде
Когда приложение растёт, весь код в одном файле превращается в кашу. MVC (Model–View–Controller) — способ разложить его по трём полкам. Мы построим магазин именно так.
| Слой | Отвечает за | У нас это файлы |
|---|---|---|
| Model (модель) | Данные: где хранятся, как читать/писать | models/products.js, models/users.js, models/db.js |
| View (представление) | Как данные выглядят — HTML | views/pages.js, views/layout.js |
| Controller (контроллер) | Склейка: взять из модели → отдать во view | controllers/shopController.js и др. |
Пройди этот путь по шагам — увидишь, как запрос «прошивает» слои и возвращается ответом:
Запрос GET /product/2 сквозь MVC
0 / 6Нажми Start — проследи, как запрос идёт по слоям.
MVC — не единственная архитектура
MVC удобен для сервера, который сам отдаёт HTML (как у нас). Но паттернов много — все решают одну задачу: разделить ответственность, чтобы код не превратился в один гигантский файл. Коротко, что ещё встретишь:
| Архитектура | Идея в одной фразе | Где встретишь |
|---|---|---|
| MVC | Model–View–Controller — три слоя, контроллер связывает | Express, Rails, Laravel, Django (наш случай) |
| Layered / N-tier | Слои: presentation → business logic → data. MVC — частный случай | Почти любой enterprise-бэкенд |
| MVVM | View связан с данными через биндинг, «ViewModel» синхронизирует | Vue, Angular (фронтенд, этот сайт) |
| Component-based (SPA) | UI из переиспользуемых компонентов, состояние в браузере | React, Vue, Svelte |
| Client–Server / REST API | Бэкенд отдаёт JSON, фронтенд рисует. Слои разнесены по сети | Мобилки + API, SPA + backend |
| Microservices | Не один сервер, а много мелких сервисов по своей задаче | Крупные системы (Netflix, банки) |
| Hexagonal / Clean | Бизнес-логика в центре, БД и UI — сменные «плагины» по краям | Большие долгоживущие проекты |
| Event-driven | Части общаются через события/очереди, а не прямыми вызовами | Обработка платежей, уведомления, стримы |
Блок 1: Первый сервер на Express
Хватит теории. Создаём проект.
1.1. Папка и зависимости
mkdir aql-shop
cd aql-shop
npm init -y
npm install express jsonwebtoken bcryptjs cookie-parser dotenv
Открой package.json и добавь "type": "module" — чтобы писать современный JS с import:
{
"name": "aql-shop",
"type": "module",
"scripts": {
"start": "node src/server.js",
"dev": "node --watch src/server.js"
}
}
1.2. Самый маленький сервер
Создай src/server.js:
Запусти:
npm start
Открой http://localhost:3000 — увидишь заголовок. Ты только что написал веб-сервер. Когда браузер запросил /, сработала функция и вернула HTML.
Блок 2.1: Модель — наши данные
Сначала данные, потом всё остальное. Создай data/db.json:
{
"products": [
{ "id": 1, "title": "Ноутбук Aqbook 14", "price": 450000, "emoji": "💻" },
{ "id": 2, "title": "Наушники SilentQ", "price": 39000, "emoji": "🎧" },
{ "id": 3, "title": "Смартфон Barys X", "price": 210000, "emoji": "📱" }
],
"users": []
}
Вот как выглядят наши данные и их связи:
2.1. Слой доступа к файлу
Создай src/models/db.js — он умеет только читать и писать весь файл:
import { readFileSync, writeFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
const __dirname = dirname(fileURLToPath(import.meta.url));
const DB_PATH = join(__dirname, "..", "..", "data", "db.json");
export function readDb() {
return JSON.parse(readFileSync(DB_PATH, "utf8"));
}
export function writeDb(data) {
writeFileSync(DB_PATH, JSON.stringify(data, null, 2), "utf8");
}
2.2. Модель товаров
src/models/products.js — понятные функции поверх файла. Контроллеры зовут getAllProducts(), не зная, что внутри JSON:
import { readDb } from "./db.js";
export function getAllProducts() {
return readDb().products;
}
export function getProductById(id) {
return readDb().products.find((p) => p.id === Number(id)) ?? null;
}
Блок 2.2: View — рисуем HTML
Никакого шаблонизатора — чистый JS. Функция принимает данные и возвращает строку HTML.
2.3. Каркас страницы
src/views/layout.js:
2.4. Страницы
src/views/pages.js:
import { layout, esc } from "./layout.js";
const tenge = (n) => `${Number(n).toLocaleString("ru-RU")} ₸`;
// user прокидываем во view, чтобы шапка (nav) знала, кто вошёл.
export function homePage(products, user) {
const cards = products
.map(
(p) => `<a class="card" href="/product/${p.id}">
<div class="card-emoji">${esc(p.emoji)}</div>
<div class="card-title">${esc(p.title)}</div>
<div class="card-price">${tenge(p.price)}</div>
</a>`,
)
.join("");
return layout({ title: "Каталог", user, body: `<h1>Каталог</h1>
<div class="grid">${cards}</div>` });
}
export function productPage(product, user) {
return layout({ title: product.title, user, body: `
<a href="/">← назад</a>
<h1>${esc(product.emoji)} ${esc(product.title)}</h1>
<p>${tenge(product.price)}</p>
<form action="/cart/add" method="post">
<input type="hidden" name="productId" value="${product.id}" />
<button>🛒 В корзину</button>
</form>` });
}
Функция productPage вернёт вот такую страницу товара с кнопкой «В корзину» (после того как добавишь стили):

Блок 2.3: Controller и роуты — оживляем каталог
Контроллер берёт данные из модели и отдаёт во view. src/controllers/shopController.js:
import { getAllProducts, getProductById } from "../models/products.js";
import { homePage, productPage } from "../views/pages.js";
export function home(req, res) {
res.send(homePage(getAllProducts(), req.user));
}
export function productDetail(req, res) {
const product = getProductById(req.params.id);
if (!product) return res.status(404).send("<h1>404</h1>");
res.send(productPage(product, req.user));
}
Теперь соберём приложение. src/server.js:
Запусти и открой каталог — карточки кликаются, товары открываются. Работает голый HTML без стилей. Логика MVC готова.
🎨 Твоя задача: оживи внешний вид
CSS я не даю — это твой кусок работы. У тебя уже есть готовые классы в HTML (.card, .grid, .btn, .detail, nav). Создай public/style.css и сделай так, чтобы каталог выглядел как настоящий магазин.
Цель — примерно так:

Пиши сам. Застрял — открывай подсказки по одной, они ведут от идеи к коду. Полный «продовый» style.css (с dark-темой, шрифтами, :hover-анимациями) лежит в референс-проекте — загляни туда только когда сделаешь свой вариант.
Куда Express положит "5" при запросе GET /product/5, если роут описан как "/product/:id"?
Блок 3.1: Пароли — никогда не храни в открытую
Пора впускать пользователей. Первое правило: пароль в базе хранить нельзя. Если базу украдут — утекут все пароли (а люди используют один пароль везде).
src/models/users.js:
import bcrypt from "bcryptjs";
import { readDb, writeDb } from "./db.js";
export function findUserByEmail(email) {
return readDb().users.find(
(u) => u.email.toLowerCase() === String(email).toLowerCase(),
) ?? null;
}
export async function createUser({ name, email, password }) {
const db = readDb();
const nextId = db.users.reduce((m, u) => Math.max(m, u.id), 0) + 1;
const passwordHash = await bcrypt.hash(password, 10); // 10 = «стоимость»
const user = { id: nextId, name, email: email.toLowerCase(),
passwordHash, role: "user", cart: [] };
db.users.push(user);
writeDb(db);
return user;
}
export async function verifyPassword(user, password) {
return bcrypt.compare(password, user.passwordHash);
}
Блок 3.2: JWT — паспорт пользователя
Пользователь ввёл пароль — сервер проверил. Но HTTP не помнит предыдущие запросы (он stateless). Как на следующем запросе понять, что это тот же вошедший человек? Ответ: выдать ему токен — подписанный «пропуск», который браузер будет присылать с каждым запросом.
Мы используем JWT (JSON Web Token) — стандарт RFC 7519. 12 Это строка из трёх частей через точку: header.payload.signature. Разбери настоящий токен:
🔓 Разбираем JWT по косточкам
Нажми на цветную часть — увидишь, что внутри
Токен = header . payload . signature
PAYLOAD → это просто base64, любой может прочитать!
{
"sub": 2,
"name": "Aruzhan",
"role": "admin",
"iat": 1719920000,
"exp": 1719923600
}⏰ exp = 1719923600 → 2024-07-02 12:33:20 UTC (когда токен протухнет)
Здесь лежат claims — факты о юзере: sub (id), role, exp. Никогда не клади сюда пароль или секрет — payload не зашифрован, а лишь закодирован.
💡 Главное: JWT не прячет данные (payload читается всеми), а защищает от подделки (подпись проверяется секретом). Секретность даёт HTTPS, целостность — подпись.
Способы аутентификации в API бывают разные — JWT в заголовке Authorization: Bearer один из самых частых:
🔑 Способы аутентификации в API
Нажми на карточку — узнай подробности
💡 В нашем боте мы используем API Key (Supabase anon key) и Bearer Token (OpenAI). Они часто идут вместе — Supabase требует оба в одном запросе.
3.2. Функции для токена
Сначала положим секрет в .env (его нельзя коммитить — добавь .env в .gitignore):
JWT_SECRET=длинная-случайная-строка-поменяй-меня
src/auth.js:
import "dotenv/config"; // читает .env и кладёт всё в process.env
import jwt from "jsonwebtoken";
const JWT_SECRET = process.env.JWT_SECRET || "dev-secret-change-me";
export function signToken(user) {
// В payload — минимум: id, имя, роль. Пароль сюда НИКОГДА.
return jwt.sign(
{ sub: user.id, name: user.name, role: user.role },
JWT_SECRET,
{ expiresIn: "2h" },
);
}
export function verifyToken(token) {
return jwt.verify(token, JWT_SECRET); // бросит ошибку, если подделан/протух
}
Блок 3.3: Middleware и роли — охрана на входе
Middleware — функция (req, res, next), которая стоит на пути запроса. Она либо пропускает дальше (next()), либо сама завершает ответ (редирект/ошибка). Из таких «прослоек» и строится вся защита.
Посмотри, как запрос от разных пользователей идёт по цепочке middleware на защищённый /admin. Отправь запрос гостем, юзером и админом — увидишь, где каждого развернут:
🛡 Кто дойдёт до /admin?
Отправь запрос от разного пользователя — смотри, где его развернут
app.use — пишет в консоль метод и путь. Всегда next().
Читает cookie с JWT. Нет токена → 401. Есть → кладёт req.user и next().
Смотрит req.user.role. Не admin → 403. Admin → next().
Контроллер /admin — отдаёт админ-панель.
Нажми на одного из трёх пользователей выше.
3.3. Пишем middleware
src/middleware/auth.js:
3.4. View формы входа
Сначала — страница с формой. Добавь в src/views/pages.js:
// Форма входа. isAdmin=true — оформляем как вход в админку.
export function loginPage({ next = "/", error = "", isAdmin = false } = {}, user) {
return layout({
title: isAdmin ? "Вход в админку" : "Вход",
user,
body: `<div class="auth">
<h1>${isAdmin ? "⚙️ Вход в админку" : "Вход"}</h1>
${error ? `<p class="error">${esc(error)}</p>` : ""}
<form action="/login" method="post">
<input type="hidden" name="next" value="${esc(next)}" />
<label>Email <input type="email" name="email" required /></label>
<label>Пароль <input type="password" name="password" required /></label>
<button class="btn">Войти</button>
</form>
</div>`,
});
}
3.5. Контроллер входа
src/controllers/authController.js:
import { findUserByEmail, verifyPassword } from "../models/users.js";
import { signToken } from "../auth.js";
import { loginPage } from "../views/pages.js";
const COOKIE_OPTS = { httpOnly: true, sameSite: "lax", maxAge: 2 * 60 * 60 * 1000 };
// GET /login — показать форму. next запоминаем, чтобы вернуть куда шли.
export function showLogin(req, res) {
const next = typeof req.query.next === "string" ? req.query.next : "/";
res.send(loginPage({ next }, req.user));
}
// GET /admin/login — та же форма, оформленная как вход в админку.
export function showAdminLogin(req, res) {
res.send(loginPage({ next: "/admin", isAdmin: true }, req.user));
}
// POST /login — проверить пароль и выдать JWT в cookie.
export async function login(req, res) {
const { email, password } = req.body;
const next = req.body.next || "/";
const user = findUserByEmail(email);
// Одинаковая ошибка на «нет юзера» и «неверный пароль» — не подсказываем врагу.
if (!user || !(await verifyPassword(user, password))) {
return res.status(401).send(loginPage({ next, error: "Неверный email или пароль" }, null));
}
res.cookie("token", signToken(user), COOKIE_OPTS);
res.redirect(next);
}
export function logout(req, res) {
res.clearCookie("token");
res.redirect("/");
}
Форму рисует loginPage из views/pages.js (полный код — в референс-проекте; он просто возвращает <form method="post" action="/login"> с полями email и пароль).
Форма входа (/login) выглядит так — обычный HTML-<form method="post">, который шлёт email и password:

3.6. Админка: модель, view и контроллер
Сначала допишем модель — в src/models/products.js добавь две функции:
export function addProduct({ title, price, emoji }) {
const db = readDb();
const nextId = db.products.reduce((max, p) => Math.max(max, p.id), 0) + 1;
const product = { id: nextId, title: String(title).trim(),
price: Number(price), emoji: emoji || "📦" };
db.products.push(product);
writeDb(db);
return product;
}
export function deleteProduct(id) {
const db = readDb();
db.products = db.products.filter((p) => p.id !== Number(id));
writeDb(db);
}
Теперь view админки — в src/views/pages.js:
export function adminPage(products, user) {
const rows = products
.map(
(p) => `<tr>
<td>${p.id}</td>
<td>${esc(p.emoji)} ${esc(p.title)}</td>
<td>${tenge(p.price)}</td>
<td><form action="/admin/products/${p.id}/delete" method="post" class="inline">
<button class="link danger">удалить</button></form></td>
</tr>`,
)
.join("");
return layout({
title: "Админка",
user,
body: `<h1>⚙️ Админка</h1>
<table class="admin">
<thead><tr><th>ID</th><th>Товар</th><th>Цена</th><th></th></tr></thead>
<tbody>${rows}</tbody>
</table>
<h2>Добавить товар</h2>
<form action="/admin/products" method="post" class="add">
<input name="emoji" placeholder="📦" maxlength="4" />
<input name="title" placeholder="Название" required />
<input name="price" type="number" placeholder="Цена" required min="0" />
<button class="btn">Добавить</button>
</form>`,
});
}
// Пригодится для 404-ответа в server.js:
export function notFoundPage(user) {
return layout({ title: "404", user,
body: `<h1>404 — не найдено</h1><p><a href="/">← на главную</a></p>` });
}
И, наконец, контроллер src/controllers/adminController.js:
import { getAllProducts, addProduct, deleteProduct } from "../models/products.js";
import { adminPage } from "../views/pages.js";
export function adminDashboard(req, res) {
res.send(adminPage(getAllProducts(), req.user));
}
export function createProduct(req, res) {
const { title, price, emoji } = req.body;
if (title && price) addProduct({ title, price, emoji });
res.redirect("/admin");
}
export function removeProduct(req, res) {
deleteProduct(req.params.id);
res.redirect("/admin");
}
3.7. Собираем всё вместе
Теперь всё соединяем в src/server.js. Это полный файл — с импортами. Порядок middleware важен, они выполняются сверху вниз:
Блок 3.4: Корзина — где пригодился requireAuth
Корзину мы уже защитили requireAuth в роутах. Осталась модель и контроллер. Корзина живёт внутри пользователя: user.cart = [productId, ...] — так она сохраняется в db.json между запусками.
src/models/carts.js:
import { readDb, writeDb } from "./db.js";
import { getProductById } from "./products.js";
export function addToCart(userId, productId) {
const db = readDb();
const user = db.users.find((u) => u.id === Number(userId));
if (!user) return;
user.cart.push(Number(productId));
writeDb(db);
}
export function removeFromCart(userId, productId) {
const db = readDb();
const user = db.users.find((u) => u.id === Number(userId));
if (!user) return;
const idx = user.cart.indexOf(Number(productId)); // убираем одну штуку
if (idx !== -1) user.cart.splice(idx, 1);
writeDb(db);
}
export function getCart(userId) {
const db = readDb();
const user = db.users.find((u) => u.id === Number(userId));
if (!user) return { items: [], total: 0 };
const items = user.cart.map((id) => getProductById(id)).filter(Boolean);
const total = items.reduce((sum, p) => sum + p.price, 0);
return { items, total };
}
View корзины — добавь cartPage в src/views/pages.js:
const tenge = (n) => `${Number(n).toLocaleString("ru-RU")} ₸`; // если ещё не объявлен выше
export function cartPage({ items, total }, user) {
const rows = items
.map(
(p) => `<li>${esc(p.emoji)} ${esc(p.title)} — ${tenge(p.price)}
<form action="/cart/remove" method="post" class="inline">
<input type="hidden" name="productId" value="${p.id}" />
<button class="link danger">✕</button>
</form></li>`,
)
.join("");
return layout({
title: "Корзина",
user,
body: `<h1>Корзина</h1>
${
items.length
? `<ul class="cart">${rows}</ul>
<p class="total">Итого: <strong>${tenge(total)}</strong></p>`
: `<p>Корзина пуста. <a href="/">В каталог</a></p>`
}`,
});
}
Контроллер в shopController.js. Обрати внимание на req.user — его положил attachUser, а requireAuth гарантировал, что он не null:
import { addToCart, removeFromCart, getCart } from "../models/carts.js";
import { cartPage } from "../views/pages.js";
export function cartView(req, res) {
res.send(cartPage(getCart(req.user.id), req.user));
}
export function cartAdd(req, res) {
addToCart(req.user.id, req.body.productId);
res.redirect("/cart"); // PRG: после POST — редирект, чтобы F5 не дублировал
}
export function cartRemove(req, res) {
removeFromCart(req.user.id, req.body.productId);
res.redirect("/cart");
}
Эти три функции добавь в тот же shopController.js, где уже лежат home и productDetail.

Блок 3.5: Ломаем и проверяем
Запусти сервер и проверь все три сценария из интерактива выше — теперь по-настоящему:
| Действие | Ожидаемый результат |
|---|---|
Гость открывает /cart | Редирект на /login (нет токена) |
Гость открывает /admin | Редирект на /login |
Вошёл как aru@shop.kz, открыл /admin | 403 — залогинен, но роль user |
Вошёл как admin@shop.kz, открыл /admin | 200 — админка открылась |
| Открыл DevTools → Application → Cookies | Видишь token, помечен HttpOnly |
Вот как выглядит 403, когда юзер с ролью user ломится в /admin — сервер узнал его, но не пустил:

Почему attachUser должен стоять ПОСЛЕ cookieParser в списке app.use?
Готово. Что дальше
За один день ты собрал настоящее веб-приложение: HTTP-сервер, MVC-архитектура, каталог, корзина, регистрация, вход по паролю с bcrypt, JWT в httpOnly-cookie, middleware и роли. Это база, на которой стоят почти все веб-сервисы.
Источники
- MDN — Critical rendering path. https://developer.mozilla.org/en-US/docs/Web/Performance/Critical_rendering_path
- J. J. Garrett, «Ajax: A New Approach to Web Applications», 2005.
- MDN — HTTP request methods. https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods
- MDN — HTTP response status codes. https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
- MDN — What is HTTPS / TLS. https://developer.mozilla.org/en-US/docs/Glossary/HTTPS
- Node.js — About / History. https://nodejs.org/en/about
- Node.js — The event loop. https://nodejs.org/en/learn/asynchronous-work/event-loop-timers-and-nexttick
- npm Docs. https://docs.npmjs.com/
- Express — Routing & Middleware. https://expressjs.com/en/guide/routing.html
- OWASP — Cross Site Scripting (XSS). https://owasp.org/www-community/attacks/xss/
- OWASP — Password Storage Cheat Sheet (bcrypt). https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html
- RFC 7519 — JSON Web Token (JWT). https://datatracker.ietf.org/doc/html/rfc7519
- OWASP — Session Management / Cookie flags. https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html
- M. Fowler, «Patterns of Enterprise Application Architecture» (MVC, Layered, Service Layer). https://martinfowler.com/eaaCatalog/