itmo_conspects

Web-разработка: Backend

Презентации доступны в репозитории https://github.com/is-web-y25/lectures

Лекция 1. Введение

Как ранее обсуждалось на курсе фронтенд-разработки, сейчас документный обмен через сеть Интернет преимущественно с помощью веб-технологий

Так браузер на компьютере клиента, используя URL, посылает запрос по HTTP-протоколу на сервер, который отвечает набором файлов, которые задает веб-страницу с помощью языков HTML, CSS и JavaScript

Все ресурсы веб-сайтов делятся на два типа:

Работа веб-сети основана на пяти ключевых стандартах:

  1. URL - способ задания адресов ресурсов сети
  2. HTTP - протокол взаимодействия между клиентами и серверами
  3. HTML - язык описания гипертекстовых документов
  4. CSS - язык форматирования гипертекстовых документов
  5. JavaScript - язык описания программ, выполняемых на стороне клиента

Протокол HTTP (HyperText Transfer Protocol, Протокол передачи гипертекста) является ключевым участником взаимодействия всех участников веб-сети

Протокол HTTP:

Веб-серверы ещё называют HTTP-серверами, браузеры - HTTP-клиентами, но клиентами могут быть и другие программы: прокси-серверы, поисковые агенты и тому подобное

HTTP работает по принципу запрос-ответ (Request-Response): клиент отправляет запрос, сервер возвращает ответ

HTTP-запрос имеет такую структуру:

МЕТОД /[имя-ресурса][?параметры-запроса] HTTP/номер-версии
Имя-заголовка-1: значение
Имя-заголовка-2: значение

[тело запроса, может отсутствовать]

Здесь:

Пример запроса:

GET / HTTP/1.1
Host: httpforever.com
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36 Edg/145.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: ru

Запрос пишется в текстовом формате, причем конец блока заголовков обозначается последовательностью \r\n\r\n. Запрос также содержит информацию о клиенте - его операционную систему (здесь это 64-битная Windows 10/11), его браузер (здесь это Microsoft Edge на базе Chromium), движок рендера (здесь это AppleWebKit), и принимаемые кодировки (например, можно заархивировать контент), форматы документов и язык контента

Некоторые строки пишутся для соблюдения совместимости. Например, Mozilla/5.0 отправляется потому, что серверы отдавали расширенный контент только браузерам Mozilla


HTTP-ответ выглядит так:

HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Sun, 08 Mar 2026 15:13:19 GMT
Content-Type: text/html
Last-Modified: Wed, 22 Mar 2023 14:54:48 GMT
Transfer-Encoding: chunked
Connection: keep-alive
ETag: W/"641b16b8-1404"
Referrer-Policy: strict-origin-when-cross-origin
X-Content-Type-Options: nosniff
Feature-Policy: accelerometer 'none'; camera 'none'; geolocation 'none'; gyroscope 'none'; magnetometer 'none'; microphone 'none'; payment 'none'; usb 'none'
Content-Security-Policy: default-src 'self'; script-src cdnjs.cloudflare.com 'self' 'report-sha256'; style-src cdnjs.cloudflare.com 'self' fonts.googleapis.com 'unsafe-inline'; font-src fonts.googleapis.com fonts.gstatic.com cdnjs.cloudflare.com; frame-ancestors 'none'; report-uri https://scotthelme.report-uri.com/r/d/csp/enforce
Content-Encoding: gzip

[[HTML-документ]]

Здесь содержится информация о типе контента, сервере (nginx/1.18.0 (Ubuntu)), нужных для сайту разрешений браузера (например, геолокация - geolocation 'none')

Первая строка HTTP-ответа содержит трёхзначный код состояния, который сообщает клиенту о результате обработки запроса. Коды делятся на пять категорий:


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


Для работы веб-приложения нужен веб-сервер - серверная программа, работающая в фоновом режиме, которая принимает HTTP-запросы от клиентов и возвращает HTTP-ответы (обычно HTML-документы)

Современные веб-серверы состоят из модулей, каждый из которых отвечает за свою задачу:

  1. Модуль разрешения запроса (или роутер, Router)
    • Определяет тип контента: статический или динамический
    • Преобразует URL-адрес в реальный путь в файловой системе
    • Проверяет аутентификацию для защищённых ресурсов
  2. Модуль обработки запроса
    • Обрабатывает статический контент (отдаёт файлы напрямую)
    • Обрабатывает динамический контент (передаёт запрос на выполнение приложению)
    • Управляет состоянием сеанса, очередями, кэшем
  3. Модуль формирования HTTP-ответа
    • Формирует заголовки ответа
    • Объединяет заголовки с результатом обработки
    • Передаёт ответ клиенту

Важно, что HTTP не поддерживает состояние сеанса. Вся информация о запросе содержится только в самом запросе (заголовках и теле)


Веб-сервер, как программе, нужно оборудование, на котором он будет работать. В качестве такого оборудования, где веб-сервер может хоститься (от host) могут выступать:

Рассмотрим платформу Render, основанную в 2018 году. Render позиционируется как современная замена Heroku и поддерживает Node.js, Python, Go, Ruby, Java, PHP и другие языки

Render используется для:

Приложения запускаются в изолированных контейнерах, при этом одно приложение может использоваться несколькими контейнерами, что обеспечивает лёгкое масштабирование

Лекция 2. Подходы к разработке веб-приложений

Все подходы к разработке веб-приложений можно разделить на 3 категории:

Программные подходы

Программные подходы состояли в создании программой на универсальном языке высокого уровня (или скрипта, который исполнялся с помощью интерпретатора)

Для этого разметка HTML и другие конструкции форматирования встраивались прямо в логику работы программы с помощью операторов вывода

Такой подход ограничивал возможность дизайнерам вносить изменения в оформление или расположение элементов, так как им пришлось открывать исходный код, знать язык программирования и другие инструменты (или помощь программиста🧑‍💻)

С помощью такого подхода можно динамически формировать содержимое страницы на HTTP-запрос. Первой такой технологией, которая позволила независимо от типа веб-сервера, была Common Gateway Interface (CGI, не путать с Computer-generated imagery), определявшая набор правил для программы, чтобы она могла выполняться для разных ОС и HTTP-серверов

Технология работала так:

  1. При поступлении HTTP-запроса определялась, какая программа должна быть запущена (чтобы обеспечивать доступ к разным ресурсам)
  2. Запускался новый процесс этой программы, переменные окружения которой содержали параметры HTTP-запроса
  3. Запускалась функция main(), которая в стандартный поток вывода выводила HTML-страницу

Выглядела она так:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {
    // Получение переменных окружения
    char *method = getenv("REQUEST_METHOD");
    char *query  = getenv("QUERY_STRING");     // данные запроса

    // HTTP-заголовок
    printf("Content-Type: text/html; charset=utf-8\r\n\r\n");

    printf("<html><body>\n");
    printf("<h1>CGI Example in C</h1>\n");

    // Информация о методе запроса
    printf("<p>Request method: %s</p>\n", method ? method : "unknown");

    // Обработка GET
    if (method && strcmp(method, "GET") == 0 && query) {
        printf("<h2>GET parameters:</h2>\n");
        printf("<pre>%s</pre>\n", query);
    }
    else {
        printf("<p>No data received or unsupported method.</p>\n");
    }

    printf("</body></html>\n");
    return 0;
}

Технология CGI позволяла использовать любой язык программирования (в том числе скриптовые, такие как Python или Perl), но были недостатки:

Следующей попыткой стала технология FastCGI - в ней вместо создания нового процесса появилась возможность использования существующего


Далее для веб-сервера Internet Information Server (IIS) от Microsoft, который поставлялся с операционной системой Windows Server, был разработан интерфейс ISAPI

Этот интерфейс позволял расширить стандартные возможности веб-сервера. ISAPI представлял библиотеку функций, с помощью которой программисты могли создавать веб-приложения в виде DLL-модулей (Dynamic-Link Library), что работало намного быстрее CGI-приложений

ISAPI-расширения могут связываться с вызовом файлов, имеющих специальные расширения или содержащимися в заданных каталогах. Также были фильтры, которые использовались для изменения функциональности сервера ISS

Такие приложения обычно использовали языки С++, Delphi или платформу .NET


Потом компания Sun Microsystems (разработчик Java) создала прикладной интерфейс Java Servlet API, который связывал веб-сервер с JVM. Виртуальная машина отвечала за выполнение сервлетов (компонент расширения функционала) и программы, которая управляла данными

В отличии от ISAPI-расширений сервлеты являются переносимыми между разными серверами, ОС и компьютерными архитектурами. Сервлеты могут исполняться одинаково в любой среде, если в ней был совместимый контейнер сервлетов (такой, как Apache Tomcat)

Подходы, основанные на шаблонах

Подходы на основе шаблонов используют в качестве объектов, доступных по URL, не скрипты, а шаблоны

Шаблоны представляют HTML-страницы, но в которых динамический контент заменен на особые теги. Сервер обрабатывает запрос, исполняет бизнес-логику и решает, что подставить вместо этих особых тегов (например, имя аккаунта или новостную ленту)

Первым таким подход стал Server Side Includes (SSI), которая позволяла встраивать особые инструкции в HTML-код:

<html>
<head><title>hello</title></head>
<body>
    <! -- #exec cgi http://mysite.org/cgi-bin/example.cgi -- >
</body>
</html>

Здесь внутри <body> встраивался контент, полученный CGI-программой


Позднее компания Macromedia (разработчик почившей платформы Flash), выкупила технологию Cold Fusion у компании Allaire Corporation братьев Аллейров

Она использовала в качестве особых тегов теги с приставкой cf:

<!DOCTYPE html>
<html>
<head>
    <title>Пример</title>
</head>
<body>
    <h1>Добро пожаловать!</h1>

    <!--- Получаем параметр name из строки запроса --->
    <cfparam name="url.name" default="гость">

    <p>Привет, <cfoutput>#url.name#</cfoutput>!</p>
</body>
</html>

Далее появился скриптовый язык PHP (рекурсивный акроним от PHP: Hypertext Preprocessor), который мог содержать вставки кода с логикой

<b>
<?php
if ($xyz >= 3) {
    $output = $myHeading;
} else {
    $output = 'DEFAULT HEADING';
}
echo $output;
?>
</b>

Такие вставки обрабатывались препроцессором PHP на стороне сервера


Далее Microsoft разрабатывает Action Server Pages (или ASP), которая объединила шаблоны и доступ к наборам OLE (Object Linking and Embedding - встраивание и линковка объектов) и COM (Component Object Model - модель компонентных объектов). Они уже позволяли получать данные из базы данных по интерфейсу ODBC (Open Database Connectivity)

В отличие от РНР, ASP не связан с одним конкретным скриптовым языком - в качестве стандартного языка используется язык Visual Basic Scripting Edition (VBScript), но может использоваться и JavaScript

Вставки в ASP оформляются в тегах <% ... %>:

<%@ Language=VBScript %>
<html>
<head>
    <title>Пример</title>
</head>
<body>
    <h1>Данные из базы данных (ODBC)</h1>

    <%
    ' Создаём объект Connection
    Dim conn, rs, sql
    Set conn = Server.CreateObject("ADODB.Connection")
    conn.Open "DSN=MyDSN;UID=blablabla;PWD=blablabla67;"

    ' SQLзапрос
    sql = "SELECT EmployeeID, FirstName, LastName, Title FROM Employees WHERE EmployeeID = 127"

    ' Выполняем запрос, получаем Recordset
    Set rs = conn.Execute(sql)

    If Not rs.EOF Then
        For Each field In rs.Fields
            Response.Write "<p>" & field.Value & "</p>"
        Next
    Else
        Response.Write "<p>Нет записей.</p>"
    End If

    ' Закрываем объекты и освобождаем ресурсы
    rs.Close
    Set rs = Nothing
    conn.Close
    Set conn = Nothing
    %>
</body>
</html>

ASP позволял писать логику и обращение к базе данных прямо в HTML-странице и была встроена в веб-сервер ISS, что делало основным выбором для создания веб-приложения на Windows


SUN в ответ создает Java Server Pages (JSP) для экосистемы Java. Она позволяла встраивать Java-код внутрь HTML-страницы:

<%@ page import="java.io.*" %>
<%! private CustomObject myObject; %>
<h1>My Heading</h1>
<%
    for(int i = 0; i < myObject.getCount(); i++) { %>
        <p>Item #<%= i %> is '<%= myObject.getItem(i) %>' . </p>
<% } %>

Такой код преобразуется в код сервлета, а HTML-разметка - в операторы вывода. Позже появилась возможность использовать JSP-теги, такие как <jsp:useBean> для внедрения зависимостей и <jsp:getProperty> для получения значения свойства

Подходы на основе объектных сред

Потом придумали объектные среды, такие как фреймворки. Фреймворки представляют платформу, определяющую структуру

Фреймворк разделяет программные модули, ответственные за создание контента (непосредственно бизнес-логика), от модулей, который ответственны за показ этого контента в определенном формате

Сейчас есть два подхода:

Шаблон MVC состоит из трех модулей:

MVC

Такое разделение веб-приложения упрощает структуру за счет более строго разделения его уровней

Таким образом, разработчик получает полный контроль над формируемым HTML-документом, и облегчается задача выполнения тестирования приложения

Примерами технологий разработки на основе MVC являются:

Лекция 3. Предметно-ориентированное проектирование

Домен (или предметная область) - это реальная сфера деятельности бизнеса. Модель - это абстракция, которая упрощённо описывает домен, предметы в нем и их взаимоотношений

На этом строится ключевая идея предметно-ориентированного проектирования (Domain-Driven Design, DDD) - приложение должно быть точной моделью реального бизнеса

Такой подход требует дополнительных ресурсов при проектировании архитектуры, однако полученная архитектура получается понятной для разработчиков, которые знакомы с языком домена, и позволяет бизнес-логике не зависеть от технических деталей и реализации


Для начала нужно обеспечить единый, “вездесущий” язык между разработчиками и бизнес-экспертами

Единый язык представляет из себя описание терминов, концептов и того, как бизнес должен работать

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

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

Далее из единого языка выделяют сущности - представления вещей в нашей проблемной области

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

Сущность обладает некоторыми свойствами:

Сущности - ключевое понятие в предметно-ориентированном дизайне, но чаще всего на уровне кода разработчики оперируют с объектами со значением (Value Object). Они:

Также сущности содержат бизнес-логику. Модель, где бизнес-логика содержится в сервисах, а не сущностях, называется анемичной и считается анти-паттерном

Также выделяют агрегат - группу сущностей и объектов со значениями, которая должна оставаться последовательно при сохранении

Далее идут сервисы - представления бизнес-процессов в предметной области. Сервис представляют поведения сущностей, у них нет внутреннего состояния, и чаще всего они соединяют множество объектов

Для сохранения состояния сущностей используют:

Также используют репозиторий для связи с агрегатом


Далее идет прикладной слой, который включает прикладные сервисы - они координируют управлением, то есть вызывают репозитории, запускают доменные сервисы, управляют транзакциями

За ним идет инфраструктурный слой. Инфраструктурные сервисы не содержат бизнес-логики, а только базовые инструменты для поддержания инфраструктуры (например, сервис для отправки сообщения по электронной почте)

Лекция 4. Разработка API

API (Application Programming Interface) - программный интерфейс приложения

Здесь интерфейс - это способы взаимодействия с этим приложением, то есть то, что нужно делать, чтобы обменяться данными с приложением или совершить какое-либо действие

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

Вследствие этого появились 2 подхода к разработке приложения:

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

Когда появились веб-сервисы, развитие получили интерфейсы веб-приложений, доступ к которым осуществлялся через Интернет и протокол HTTP, что позволило не зависеть от языка программирования, на котором было создано веб-приложение

Всего выделилось несколько форматов веб-интерфейсов:

Чтобы сторонние разработчики могли воспользоваться каким-либо API, нужно им рассказать то, как интерфейс устроен. Для этого существует документация - описание того, что и куда нужно отправить, чтобы получить желаемый результат


Сейчас большинство веб-интерфейсов представлено в архитектурном стиле REST (так называемые RESTful API), который был описан Роем Филдингом в 2000 году

Ключевая идея REST в том, что у всех доступных ресурсов (таких как пользователи, статьи, продукты и так далее) есть свой URL, и над каждым таким ресурсом действие делается через HTTP-метод по соответствующему URL

Например, GET-запрос по URL http://example.com/users?limit=100 вернет первых 100 пользователей приложения. Здесь:

В качестве формата передачи данных для REST чаще всего используют JSON (так как он удобен для обработки JavaScript-кодом), но ничего не мешает использовать другие форматы

Также Рой Филдинг описал 5 требований к архитектуре REST API:

  1. Модель “Клиент-Сервер” - отделение потребности интерфейса клиента от потребностей сервера, хранящего данные, повышает переносимость кода клиентского интерфейса на другие платформы, а упрощение серверной части улучшает масштабируемость.

  2. Отсутствие состояния (сессии клиента) - между запросами сервер не должен хранить состояние клиента, то есть запрос сам по себе должен содержать всю необходимую информацию

  3. Кэширование - должно быть явное обозначение кэшируемых и некэшируемых ответов, чтобы клиент получал актуальную информацию

  4. Унифицированный интерфейс - все ресурсы идентифицируются в запросах; каждое сообщение содержит достаточно информации, чтобы понять, каким образом его обрабатывать; если клиент хранит представление ресурса, включая метаданные, то он обладает достаточной информацией для модификации или удаления ресурса

  5. Прозрачность слоев - клиент не способен определить, общается он напрямую с сервером или с промежуточным слоем

Для упрощения документирования REST-интерфейсов разработали спецификации OpenAPI и RAML. Благодаря им, можно описать, что делает каждый метод, какие параметры ему нужны и так далее, а затем сгенерировать готовый документ

Рассмотрим OpenAPI - спецификация позволяет описать API на языке YAML или JSON:

openapi: 3.0.0
info:
  version: 1.0.0
  title: Swagger Petstore
  license:
    name: MIT
servers:
  - url: http://petstore.swagger.io/v1
paths:
  /pets:
    get:
      summary: List all pets
      operationId: listPets
      tags:
        - pets
      parameters:
        - name: limit
          in: query
          description: How many items to return at one time (max 100)
          required: false
          schema:
            type: integer
            maximum: 100
            format: int32
      responses:
        '200':
          description: A paged array of pets
          headers:
            x-next:
              description: A link to the next page of responses
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Pets'
        default:
          description: unexpected error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    # ...
components:
  schemas:
    Pet:
      type: object
      required:
        - id
        - name
      properties:
        id:
          type: integer
          format: int64
        name:
          type: string
        tag:
          type: string
    Pets:
      type: array
      maxItems: 100
      items:
        $ref: '#/components/schemas/Pet'
    Error:
      type: object
      required:
        - code
        - message
      properties:
        code:
          type: integer
          format: int32
        message:
          type: string

Здесь описаны:

Затем такие инструменты, как Swagger, могут сгенерировать HTML-страницу с документацией:

Petstore

Лекция 5. Бэкенд для фронтенда

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

Такой подход работает, но со временем у него появляются ограничения:

Но можно внести ряд улучшений:

  1. Заменить JSON на GraphQL. Таким образом, мы убираем лишнюю логику, отвечающую за представление данных, а клиенты могут выбирать, какие именно данные им нужны
  2. Вынести рендеринг HTML-документов на отдельный сервер, который будет обращаться за данными к бэкенд-серверу
  3. Разбить бэкенд на множество микросервисов

В итоге получаем, что в каждом сервисе нет зависимости от языка и от фреймворка, а бэкенд-разработчик не взаимодействует с сервисами, которые ему не требуются

Так появляется архитектурный паттерн “бэкенд для фронтенда” - в нем для каждого уникального фронтенда есть свой бэкенд. Этот бэкенд будет заниматься:

Серверный фреймворк Nest для Node.js отлично подходит для этой задачи. Его архитектура включает:

Также Nest концептуально похож на Angular:

Базовые элементы Nest

Архитектура Nest строится вокруг модулей. Модуль объединяет связанные части приложения:

Контроллер принимает HTTP-запрос и возвращает ответ. В нем не стоит хранить сложную бизнес-логику: обычно он лишь принимает параметры, вызывает сервис и возвращает результат.

import { Controller, Get, Param, ParseIntPipe } from '@nestjs/common';
import { UsersService } from './users.service';

@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get(':id')
  findOne(@Param('id', ParseIntPipe) id: number) {
    return this.usersService.findOne(id);
  }
}

Здесь:


Сервис обычно содержит прикладную или бизнес-логику:

import { Injectable, NotFoundException } from '@nestjs/common';

@Injectable()
export class UsersService {
  private readonly users = [
    { id: 1, name: 'Alice' },
    { id: 2, name: 'Bob' },
  ];

  findOne(id: number) {
    const user = this.users.find((item) => item.id === id);

    if (!user) {
      throw new NotFoundException('Пользователь не найден');
    }

    return user;
  }
}

@Injectable() означает, что класс можно зарегистрировать в контейнере зависимостей и внедрять в другие классы


Модуль описывает, какие контроллеры и провайдеры относятся к одной области приложения.

import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';

@Module({
  controllers: [UsersController],
  providers: [UsersService],
})
export class UsersModule {}

Запуск HTTP-приложения обычно происходит в main.ts:

import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,
      transform: true,
    }),
  );

  await app.listen(3000);
}

bootstrap();

Глобальный ValidationPipe здесь:


Одна из ключевых идей Nest - внедрение зависимостей. Если сервису нужен другой сервис, его можно не создать вручную, а получить от контейнера:

constructor(private readonly usersService: UsersService) {}

Плюсы такого подхода:

В Nest зависимости обычно называются провайдерами


Для описания входных данных обычно используют DTO (Data Transfer Object)

import { IsEmail, IsString, MinLength } from 'class-validator';

export class CreateUserDto {
  @IsEmail()
  email: string;

  @IsString()
  @MinLength(2)
  name: string;
}

Использование в контроллере:

import { Body, Controller, Post } from '@nestjs/common';

@Controller('users')
export class UsersController {
  @Post()
  create(@Body() dto: CreateUserDto) {
    return dto;
  }
}

Валидация в Nest обычно строится так:

Пайпы в Nest - это специальные классы, которые могут:

Примеры встроенных pipe: ValidationPipe, ParseIntPipe, ParseBoolPipe, ParseUUIDPipe

Продвинутые элементы Nest

Охраняющие декораторы (или гуарды, от guard) проверяют, можно ли вообще выполнять обработчик запроса

Типичный пример - это проверка JWT (JSON Web Token) для авторизации и ролей пользователя

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    return Boolean(request.headers.authorization);
  }
}

Использование:

@UseGuards(AuthGuard)
@Get('profile')
getProfile() {
  return { ok: true };
}

Интерцептор оборачивает вызов обработчика. Он может:

import {
  CallHandler,
  ExecutionContext,
  Injectable,
  NestInterceptor,
} from '@nestjs/common';
import { map, Observable } from 'rxjs';

@Injectable()
export class ResponseInterceptor implements NestInterceptor {
  intercept(
    context: ExecutionContext,
    next: CallHandler,
  ): Observable<unknown> {
    return next.handle().pipe(
      map((data) => ({
        data,
        timestamp: new Date().toISOString(),
      })),
    );
  }
}

Фильтры перехватывают исключения и преобразуют их в HTTP-ответ:

import {
  ArgumentsHost,
  Catch,
  ExceptionFilter,
  HttpException,
} from '@nestjs/common';

@Catch(HttpException)
export class HttpErrorFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const response = host.switchToHttp().getResponse();
    const status = exception.getStatus();

    response.status(status).json({
      statusCode: status,
      message: exception.message,
    });
  }
}

Это полезно, например, для создания шаблонных страниц с ошибками, такими как HTTP 404


Промежуточное ПО (Middleware) выполняется раньше, чем гуарды и контроллер. Он удобен для:

import { Injectable, NestMiddleware } from '@nestjs/common';
import { NextFunction, Request, Response } from 'express';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    console.log(`${req.method} ${req.originalUrl}`);
    next();
  }
}

Nest хорошо интегрируется с ORM и драйверами баз данных. Часто используют библиотеки:

Для этого реализуют репозиторий - абстракцию доступа к данным. Репозиторий скрывает детали хранения и предоставляет понятный интерфейс для предметной области. Например:

interface UsersRepository {
  findById(id: number): Promise<User | null>;
  save(user: User): Promise<void>;
}

При работе с базой данных часто различают два подхода:

Nest поддерживает оба подхода через выбранный инструмент доступа к данным


Кэширование в бэкенде для фронтенда особенно полезно, если один и тот же клиент часто запрашивает одинаковые агрегированные данные. В Nest кэширование можно подключать через менеджер кэширования, а в качестве внешнего хранилища часто используют Redis

Redis полезен, когда нужно:


Для документирования HTTP API в Nest часто используют Swagger-модуль, который строит OpenAPI-описание

Пример настройки:

import { NestFactory } from '@nestjs/core';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  const config = new DocumentBuilder()
    .setTitle('Users API')
    .setDescription('Документация сервиса пользователей')
    .setVersion('1.0')
    .build();

  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('api', app, document);

  await app.listen(3000);
}

bootstrap();

Для описания DTO используют декораторы:

import { ApiProperty } from '@nestjs/swagger';

export class CreateUserDto {
  @ApiProperty({ example: 'user@example.com' })
  email: string;
}

В Nest можно создавать свои декораторы, чтобы повторно использовать типичную логику. Например, чтобы получать текущего пользователя из запроса:

import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const CurrentUser = createParamDecorator(
  (_data: unknown, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    return request.user;
  },
);

Использование:

@Get('me')
getMe(@CurrentUser() user: User) {
  return user;
}

На практике Nest часто используется как API-слой для фронтенда. Он может:

Для фронтенда это удобно, потому что:

Лекция 6. GraphQL и Prisma

Prisma

Prisma - объектно-реляционное отображение (Object Relational Mapping, ORM) с открытым исходным кодом для экосистемы Node.js и TypeScript

Prisma состоит из следующих компонентов:

Prisma упрощает работу с базой данных, а именно моделированием данных, миграцией и написанием запросов

Моделирование данных указывается в файле с расширением .prisma:

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id Int @id @default(autoincrement())
  email String @unique
  name String?
  createdAt DateTime? @default(now()) @map("created_at")
  posts Post[]
@@map("users")
}

model Post {
  id Int @id @default(autoincrement())
  title String
  content String?
  user User @relation(fields: [authorId], references: [id])
  authorId Int? @map("author_id")
  published Boolean? @default(false)
  createdAt DateTime? @default(now()) @map("created_at")
@@map("posts")
}

Каждая из этих моделей описывает таблицу в соответствующей базе данных и служит основой для сгенерированного доступа к данным с интерфейсом, который предоставляет Prisma Client

Далее Prisma Migrate преобразует эту схему в SQL-запросы, необходимые для создания и изменения таблиц в базе данных. Чтобы сделать миграцию на базе данных, нужно применить команду npx prisma migrate

Например, схема выше превратится в такие запросы на диалекте PostgreSQL:

CREATE TABLE users (
  id SERIAL PRIMARY KEY,
  email VARCHAR(255) NOT NULL UNIQUE,
  name VARCHAR(100),
  created_at TIMESTAMP DEFAULT NOW()
);

CREATE TABLE posts (
  id SERIAL PRIMARY KEY,
  title VARCHAR(200) NOT NULL,
  content TEXT,
  author_id INTEGER REFERENCES users(id),
  published BOOLEAN DEFAULT false,
  created_at TIMESTAMP DEFAULT NOW()
);

Основным преимуществом работы с Prisma Client является то, что он позволяет разработчикам мыслить объектами и поэтому предлагает привычный и естественный способ рассуждать о своих данных. Prisma Client помогает сформировать запросы к базе данных, которые всегда возвращают простые объекты JavaScript

Помимо этого, если использовать TypeScript, результаты запросов получаются типизированными, что повышает типобезопасность

Отдельное приложение Prisma Studio позволяет управлять базой данных из интерфейса

GraphQL

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

GraphQL был создан в Facebook в 2012 году и был публично выпущен в 2015. Его основная цель - решить проблемы и ограничения, связанные с интерфейсами в стиле REST

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

Такие проблемы, когда сервис возвращает больше данных, чем клиенту нужно (избыточность или overfetching), и когда клиенту нужно несколько запросов, чтобы получить нужные данные (недостаточность или underfetching), присуще REST API

GraphQL способен по схеме запроса, переданной лишь по одному эндпоинту, понимать, какие данных из каких источников нужно взять, и возвращать их

GraphQL API обычно построен из трех компонентов:

Кроме чтения данных, GraphQL поддерживает изменение данных через мутации. Пример мутации для создания поста:

mutation {
  createPost(
    title: "Новый пост"
    content: "Текст поста"
    authorId: 1
  ) {
    id
    title
    published
  }
}

На практике GraphQL часто используют вместе с Prisma:

Лекция 7. Аутентификация

Идентификация - процедура распознавания субъекта по его идентификатору

Компьютеры не умеют распознавать людей или другие объекты, поэтому нужна вещь, которая бы олицетворяла субъект в информационной системе. Идентификатором может быть номер телефона, номер паспорта или адрес электронной почты

Аутентификация - процедура проверки подлинности субъекта. Проверить можно тремя способами:

Авторизация - это предоставление доступа к какому-либо ресурсу, например, к панели управления магазина

Обычно, чтобы усилить безопасность, применяют многофакторную аутентификацию - в ней субъекту нужно несколькими способами подтвердить, что именно он является пользователем. Наиболее распространена двухфакторная аутентификация (2FA или Two-Factor Authentication): например, сначала пользователь вводит пароль от учетной записи, а затем прикладывает отпечаток пальца

Также выделяют многоэтапную аутентификацию - процесс, в котором проверяется подлинность по одному фактору несколько раз, например, по паролю и пришедшему коду на электронную почту

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


Стандартная схема аутентификации по логину и паролю применительно к веб-приложениям может имплементироваться разными способами

Аутентификация по HTTP

При обращении к защищенному ресурсу сервер отправляет HTTP-ответ, содержащий статус 401 Unauthorized и заголовок WWW-Authenticate, где указана схема аутентификации

Далее браузер отображает свое диалоговое окно, где пользователь вводит логин (например, username) и пароль (password). После этого браузер каждый раз при доступе к ресурсу отправляет эти данные в заголовке Authorization в HTTP-запросе. После этого сервер решает, предоставлять доступ или нет

Аутентификация по HTTP

Сами логин и пароль могут храниться внутри заголовка несколькими способами (их называют схемами):

  1. Basic (Базовая схема)

    В ней данные аутентификации представляются в виде строки username:password в кодировке Base64, использующей буквы латинского алфавита, цифры и 2 специальных символа

    Кодировка Base64 обратима, поэтому, если общение идет через протокол HTTP, то логин и пароль видны всем участникам сети.

    По этой причине схему Basic нужно всегда использовать в связке с HTTPS, который шифрует заголовки

  2. Digest (от англ. переваривать)

    Схема Digest работает так: сервер шлет клиенту уникальное число nonce, далее клиент использует username, password, nonce, URI ресурса и другие параметры для вычисления хеша, используя функцию MD5 или SHA-256

    Такая схема лучше, чем Basic, но уязвима к атакам “человек посередине” (man-in-the-middle attack, MITM): посредник может перехватить ответ сервера, а котором он говорит использовать схему Digest, и отправить клиенту ответ с просьбой использовать Basic, тем самым получив его пароль

  3. NTLM (New Technology LAN Manager)

    NTLM - семейство протоколов аутентификации, созданный компанией Microsoft

    В нем пароль не передается напрямую, а тоже как хеш от пароля и случайного числа сервера

    NTLM встроен в экосистему Windows и преимущественно используется для аутентификации пользователей Windows Active Directory

    NTLM не защищен к атаке на хеш, ретрансляции аутентификации и подбору пароля

  4. HTTP Negotiate

    Далее появился протокол Kerberos, который намного безопаснее NTLM, из-за чего появилась схема Negotiate - клиент, отправляя запрос серверу, передает, какой протокол он поддерживает NTLM или Kerberos

    HTTP Negotiate также называют SPNEGO (Simple and Protected GSS-API Negotiation Mechanism)

    Также Kerberos поддерживает технологию единого входа (Single Sign-On, SSO): при переходе из одного портала в другой пользователь может повторно не проходить аутентификацию, используя токен первого портала

Важно заметить, что при использовании HTTP-аутентификации единственный способ выйти из учетной записи - это закрыть окно браузера

Аутентификация с помощью формы

Далее пришли к тому, что можно создать форму на HTML-странице, где пользователь вводит логин, пароль (и дополнительно другие данные), которые отправляются на сервер для аутентификации. В случае успеха веб-приложение генерирует токен сессии (число, по которому происходит авторизация) и сохраняет его в куки. После этого при каждом запросе клиент отправляет на сервер куки, содержащие токен сессии

Аутентификация с помощью формы

Токен сессии создается двумя способами:

  1. Как идентификатор сессии, которая хранится в памяти сервера и в базе данных. Сервер содержит всю информацию о сессии (такую, как браузер, устройство, пользователь), а клиент знает только идентификатор
  2. Как зашифрованный объект, содержащий данные о пользователе

    Такой подход позволяет реализовать stateless-архитектуру сервера (без хранения состояний), однако требует механизма обновления токена по истечении срока действия

Аутентификация по сертификату

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

  1. Есть центр сертификации (Certificate Authority, CA) - доверенный сервер, который выдает сертификаты. У него есть два ключа - публичный и приватный
  2. Клиент (и опционально сервер) имеет свои публичный и приватный ключи

    Клиент (или сервер) запрашивает свой сертификат у центра сертификации - для этого создается запрос подписи сертификата (Certificate Signing Request, CSR). Этот запрос подписи подписывается приватным ключом клиента (или сервера): от всего запроса берется хеш, который шифруется с помощью приватного ключа

    Центр сертификации, зная публичный ключ клиента, переданный в запросе, может расшифровать хеш и сопоставить с тем, что получился из всего сертификата, чтобы проверить, что клиент является владельцем ключей

  3. Далее центр сертификации проверяет, является ли клиент (или сервер) владельцем тем, кто указан в запросе. После этого составляется сертификат с уникальным номером, который подписывается приватным ключом центра сертификации

Аутентификация по сертификату

По стандарту X.509 сертификат содержит:

Приватные ключи клиента и центра сертификации остаются на соответствующих устройствах (например, на компьютере или специальной USB-флешке) и не сообщаются никому, тогда как публичные свободно передаются

У такого подхода есть недостатки:

Проверка по сертификату используется в протоколе TLS (Transport Layer Security), который основан на устаревшем протоколе SSL (Secure Sockets Layer) и на основе которого работает HTTPS (HTTP Secure):

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

    • ключа центра сертификации
    • действительности даты
    • и включения в список недействительных сертификатов (если те были отозваны до срока действия)

Аналогично работает mTLS (Mutual TLS) - клиент и сервер обмениваются своими сертификатами, проверив которые, они могут убедиться, что за клиентом стоит подлинный владелец, а сервер не выдает себя за злоумышленника

Аутентификация по одноразовому паролю

Вместе с аутентификацией по паролю для большей безопасности применяют аутентификацию по одноразовому паролю. При таком подходе в случае ввода правильного пароля приложение попросит пользователя ввести одноразовый пароль

Такой пароль может быть создан:

Одноразовые пароли обычно используются для подтверждения важных операций

Аутентификация по ключу доступа

Для API применяют другой подход: вместо логин и пароля ключ доступа (или API-ключ), который является длинной уникальной строкой и по сути заменяет логин и пароль

При идентификации сервер генерирует такой ключ пользователю, который дальше сохраняет его и пользуется им для доступа к сервису. Сервер же может ввести статистику операций, которые производил пользователь с этим ключом (например, для выставления лимита запросов)

Такой ключ:

Такой ключ может быть передан в URL-запросе, HTTP-заголовке или в теле запроса и обязательно в безопасном соединении через HTTPS

Для небезопасных соединений ключ состоит из двух частей: приватная и публичная. При запросе сервер отправляет уникальное число nonce (или можно использовать временную метку), которую клиент подписывает своим приватным ключом. Сервер с помощью публичного ключа может проверить подлинность клиента

Аутентификация по токену

В другом подходе применяются токены. Приложение может делегировать аутентификацию другому приложению, а пользователя попросить войти в изначальное приложение через другое (так работает аутентификация через Google и другие сервисы)

Приложение, которое предоставляет аутентификацию (так называемый поставщик идентификации, Identity Provider), предоставляет достоверные сведения о пользователе в виде токена, а стороннее приложение использует этот токен для аутентификации и авторизации. Пользователю, чтобы войти в стороннее приложение, нужно пройти аутентификацию в поставщике идентификации, который генерирует токен для стороннего приложения

Аутентификация по токену

В браузере же веб-приложения способны перенаправлять на сайт поставщика и обратно на сайт стороннего приложения

Сам токен содержит информацию о том, кто его сгенерировал, кто получатель, срок действия и так далее, а также подпись токена. При аутентификации проверяется, что токен выдан доверенным поставщиком идентификации, токен предназначен этому приложению, срок токена еще не истек, а подпись действительна


Есть несколько форматов токена:


Разберем стандарты, определяющие аутентификацию по токену, взаимодействие между приложения и протоколы:

Лекция 8. Подписочная модель обмена сообщениями

Протокол HTTP устроен так, что взаимодействие между сервером и клиентом инициируется клиентом. Зачастую сервер ничего не знает о том, как подключиться к клиенту

Поэтому есть несколько способов передавать сообщения с сервера клиенту

Частые опросы

Частые опросы (или поллинг, Periodic polling) - самый простой способ

Работает так: надо сказать клиенту, чтобы он отправлял запрос на получение новых сообщений с сервера, например, каждые 10 секунд. В ответ сервер понимает, что клиент находится в сети, и отправляет все уведомления, которые у него накопились на этот момент

У такого подхода есть недостатки:

Длинный опрос

Длинный опрос (Long polling) - простой способ поддерживать соединение с сервером, не используя другие протоколы. Работает это так:

  1. Клиент отправляет запрос на сервер
  2. Сервер, не закрывая сетевое соединение, ждет, пока появится сообщение клиенту
  3. Когда появляется, сервер отсылает его и закрывает соединение
  4. Клиент отправляет новый запрос

Если соединение в каком-либо случае потеряно (например, при переключении Wi-Fi), то браузер пытается заново открыть соединение с сервером

Клиентский код для длинного опроса может выглядеть так:

async function subscribe() {
    const response = await fetch("/subscribe");

    if (response.status in [502, 504]) {
        // Таймаут соединения
        await subscribe();
    } else if (response.status != 200) {
        // Неправильный код ошибки
        console.log(response.statusText);

        await new Promise(resolve => setTimeout(resolve, 1000));
        await subscribe();
    } else {
        // Успешная передача
        const data = await response.json();
        console.log(data);
        await subscribe();
    }
}

Такой подход хорошо работает, когда сообщения приходят редко

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

WebSocket

WebSocket (или WS сокращенно) - протокол для совершения постоянного взаимодействия между сервером и клиентом

Протокол WebSocket особенно полезен для сервисов, которые ведут постоянное общение с клиентом, например, игры, торговые площадки и другие

Чтобы создать подключение, нужно

let socket = new WebSocket("wss://example.com")

Здесь протокол wss - это безопасное соединение (от WebSocket Secure) с использованием TLS. Аналогично ws - это протокол без TLS

Далее такой сокет генерирует 4 события:

Через сокет сообщения можно посылать через метод send

Само соединение устанавливается с помощью протокола HTTP. Клиент отправляет HTTP-запрос с заголовками Connection: Upgrade и Upgrade: websocket, тем самым спрашивая, может ли сервер перейти на общение по протоколу WebSocket. Если сервер поддерживает WebSocket, то он отправляет ответ со статусом 101 Switching Protocols. Такая процедура называется обновлением протокола

Поток данных в протоколе WebSocket состоит из фреймов - фрагментов данных, которые могут быть отправлены любой стороной. Фреймы могут быть:

Сам по себе WebSocket не описывает функционал переподключения, аутентификацию и другое, но зато это реализуют библиотеки, например, Socket.IO

Socket.IO - библиотека JavaScript, использующая веб-сокеты или другие технологии (например, Flash Socket), если веб-сокеты не доступны

Также, в отличие от веб-сокетов Socket.IO:

Лекция 9. Сборщики

Сборщик (или бандлер, bundler) - это инструмент, который берёт множество файлов проекта (JavaScript-модули, CSS, изображения, шрифты) вместе с внешними зависимостями и объединяет их в один или несколько оптимизированных файлов для отправки клиенту в браузер

Сборщик обладает преимуществами перед обычной отправкой файлов:

Сборщик работает примерно так:

  1. Разработчик указывает главный файл (например, index.js)
  2. Бандлер анализирует все import и строит полное дерево зависимостей модулей
  3. К каждому файлу применяются необходимые трансформации (транспиляция из TypeScript в JavaScript, компиляция SCSS в CSS и тому подобное)
  4. Все модули объединяются в итоговый бандл (один или несколько файлов)
  5. Готовые статические файлы сохраняются на сервере, откуда они готовы к отправке клиенту

Рассмотрим несколько популярных сборщиков

Webpack

Webpack - самый популярный и давно существующий сборщик модулей для JavaScript-приложений. Несмотря на появление более быстрых конкурентов, в 2026 году Webpack остаётся стабильным, хорошо поддерживаемым и широко используемым инструментом, особенно в крупных корпоративных проектах

Ключевые особенности:

Webpack опубликовал дорожную карту, сфокусированную на трёх направлениях:

Webpack остаётся выбором для проектов, где требуется максимально тонкая и нестандартная настройка сборки, сложное разделение кода или где уже существует большая кодовая база и экосистема плагинов, которую сложно заменить

Vite

Vite - современный инструмент для фронтенд-разработки, созданный Эваном Ю (автором Vue.js). С момента появления Vite стремительно набрал популярность благодаря молниеносной скорости запуска сервера для разработки и горячей замены модулей

До версии 8 Vite использовал два разных сборщика: esbuild (для быстрой компиляции в режиме разработки) и Rollup (для продакшн-сборки). Такой подход имел минус: два конвейера обработки порождали расхождения и требовали синхронизации

В Vite 8 оба конвейера заменены на единый бандлер Rolldown, созданный командой VoidZero на языке Rust

Ключевые преимущества Vite 8:

Параллельно с Vite 8 команда VoidZero анонсировала Vite+ - унифицированную платформу, объединяющую сборку, пакетный менеджер и среду разработки в единую экосистему на базе инструментов, написанных на Rust

Vite - идеальный выбор для новых проектов, где важна скорость запуска и мгновенная обратная связь при разработке. Особенно хорошо подходит для одностраничных приложений (Single-page application), приложений на Vue/React/Svelte и любых задач, где не требуется глубокая кастомизация сборочного процесса

Rsbuild

Rsbuild - это слой поверх Rspack (Rust-реализации, совместимой с Webpack), созданный командой ByteDance. Его цель - предоставить максимально быструю сборку при сохранении совместимости с экосистемой Webpack

Ключевые нововведения:

Rsbuild особенно привлекателен для команд, которые хотят мигрировать с Webpack и получить прирост скорости на порядок, но при этом сохранить привычную экосистему плагинов и не переписывать конфигурацию с нуля.

Module Federation

Module Federation - это архитектурный паттерн и технология (изначально представленная в Webpack 5), позволяющая одному JavaScript-приложению динамически загружать код из другого приложения прямо во время выполнения. В отличие от обычного импорта пакета, это обмен компонентами, логикой и ресурсами между независимо развёртываемыми проектами

Цели, которые преследовали разработчики:

Версия Module Federation 2.0 стала результатом переработки внутренней архитектуры командой ByteDance в сотрудничестве с Заком Джексоном (автором оригинальной технологии)

Ключевые новшества:

На 2026 год Module Federation 2.0 активно используется в экосистеме ByteDance и за её пределами. Для Next.js официальной поддержки пока нет (рекомендуемый фреймворк - Modern.js от той же команды). В планах у разработчиков создание Module Federation 3.0 и Native ESM Federation


Подытожим:

| Инструмент | Сильные стороны | Лучше всего подходит для | |-|-|-| | Webpack | Максимальная гибкость, зрелая экосистема, нейтральное управление | Крупные сложные проекты с нестандартными требованиями | | Vite 8 | Мгновенный старт сервера для разработки, высочайшая скорость сборки | Новые одностраничные приложения, быстрая разработка, Vue/React/Svelte-проекты | | Rsbuild 2.0 | Совместимость с Webpack, но со скоростью языка Rust🦀, минимум конфигурации | Миграция с Webpack, когда нужна скорость и привычный API | | Module Federation 2.0 | Динамическая загрузка кода между приложениями во времени исполнения | Микрофронтенд-архитектуры, независимо развёртываемые команды |