diff --git a/src/content/docs/chapters/2-webmap.mdx b/src/content/docs/chapters/2-webmap.mdx
index c572c49..0d3f3f8 100644
--- a/src/content/docs/chapters/2-webmap.mdx
+++ b/src/content/docs/chapters/2-webmap.mdx
@@ -12,6 +12,8 @@ import Question from '../../../components/Question.astro'
- клиент и сервер
- HTML, CSS, JavaScript
+В рамках практической части создадим карту мира на основе статических GeoJSON-файлов.
+
## Карты в Интернете
diff --git a/src/content/docs/chapters/3-api.mdx b/src/content/docs/chapters/3-api.mdx
index 92d6555..a0f61e2 100644
--- a/src/content/docs/chapters/3-api.mdx
+++ b/src/content/docs/chapters/3-api.mdx
@@ -14,6 +14,8 @@ import Question from '../../../components/Question.astro'
- асинхронность JavaScript
- запрос `fetch...then`
+В рамках практической части создадим карту вакансий на основе изменяющегося содержания Google-таблицы.
+
## API
API обычно переводят как прикладной программный интерфейс или программный интерфейс приложений. На практике чаще говорят просто "апи".
diff --git a/src/content/docs/chapters/4-backend.mdx b/src/content/docs/chapters/4-backend.mdx
new file mode 100644
index 0000000..4cf3506
--- /dev/null
+++ b/src/content/docs/chapters/4-backend.mdx
@@ -0,0 +1,351 @@
+---
+title: Бэкенд
+---
+
+import { Card, FileTree, LinkCard, Tabs, TabItem } from '@astrojs/starlight/components';
+
+В этой главе
+
+- понятия бэкенда и фронтенда
+- SQL
+- Flask
+- CORS
+
+В рамках практической части создадим карту индекса качества городской среды по базе данных SQLite.
+
+## Бэкенд
+
+Мы уже знаем, что веб-приложения можно разделить на клиентскую и серверную части. Разработку клиентской части называют фронтендом. Разработку серверной части называют бэкендом. Фронтенд общается с бэкендом через API. Бэкенд предоставляет метода API, а фронтенд к ним обращается.
+
+Когда происходит вызов метода API -- запрос определённого URL -- выполняется соответствующая серверная функция. Для программирования серверных функций могут использоваться различные языки программирования Python, Go, Rust и даже JavaScript (NodeJS).
+
+В предыдущем занятии мы обращались к бэкенду через API, а в этот раз разработаем бэкенд сами. Наш бэкенд мы разработаем на языке Python с использованием библиотеки Flask.
+
+## Подготовка
+
+Создадим папку с заготовкой для нашей карты.
+
+Добавим туда папку `backend`. Загрузим [отсюда](https://github.com/gtitov/flask-maplibre-map/raw/refs/heads/main/backend/cities_index.sqlite) и положим туда базу данных. И создадим в этой папке файл с нашим бэкендом `app.py`.
+
+
+- backend/
+ - app.py
+ - cities_index.sqlite
+- index.html
+- style.css
+- main.js
+
+
+
+Что это за база данных такая -- SQLite
+
+SQLite -- встраиваемая база данных, которая хранит все свои данные в одном файле. В одном файле хранятся все таблицы, индексы и другая информация о базе данных, что упрощает управление и резервное копирование. Благодаря этому, она не требует отдельного сервера и легко интегрируется в различные приложения.
+
+
+
+## Разработка бэкенда
+
+### Установка Flask
+
+Установим Python и после установки через терминал загрузим в Python библиотеку Flask.
+
+```sh title=Терминал
+pip install Flask
+```
+
+Теперь откроем ранее созданный нами файл `backend/app.py`. Подключим необходимые библиотеки и создадим объект `app`, в рамках которого мы будем определять доступные методы API.
+
+```py title="app.py"
+from flask import Flask, Response
+import sqlite3
+import json
+import time
+
+
+app = Flask(__name__)
+
+DB_LOCATION = "cities_index.sqlite"
+```
+
+### Список городов по году
+
+Добавим первый метод API. Он возвращает пользователю все города за выбранный год. При обращении к этому методу бэкенд выполняет запрос к базе данных и формирует на основе ответа GeoJSON файл, который мы сможем сразу отправить на карту.
+
+> Мы могли бы отправить на фронтенд и неподготовленный файл. Собрать GeoJSON на клиентской стороне, как в карте вакансий, когда Google возвращал нам CSV. Но то был чужой API. А этот наш. И в нашем мы можем сделать так, как будет удобнее нам!
+
+```py title=app.py
+...
+@app.route("/cities/") # путь API, к которому обращается пользователь
+def cities_by_year(year): # функция, которая будет выполняться при обращении
+ # start_time = time.time()
+ db = sqlite3.connect(DB_LOCATION) # подключение к базе данных
+ db.row_factory = sqlite3.Row # указание, что в строках мы будем сохранять название колонки и значение
+ cursor = db.execute("SELECT * FROM cities WHERE year = ?", (year,)) # выполняем запрос к базе, подставляя год, введённый пользователем
+ cities = cursor.fetchall() # забираем результат запроса
+ cursor.close() # закрываем запрос
+ db.close() # закрываем подключение
+ geojson = { # приводим к формату GeoJSON
+ "type": "FeatureCollection",
+ "features": [
+ {
+ "type": "Feature",
+ "geometry": {
+ "type": "Point",
+ "coordinates": [row["longitude"], row["latitude"]],
+ },
+ "properties": dict(row),
+ }
+ for row in cities
+ ],
+ }
+ r = Response( # формируем ответ
+ json.dumps(geojson, ensure_ascii=False), # ensure_ascii=False, чтобы нормально отображалась кириллица
+ mimetype="text/json" # указываем тип данных
+ )
+ # print("--- %s seconds ---" % (time.time() - start_time))
+ return r
+```
+
+Запускаем бэкенд на локальном сервере для проверки. Открываем терминал в папке `backend` и выполняем команду
+
+```sh title=Терминал
+flask run --debug
+```
+
+После чего увидим что-то вроде
+
+```sh title=Терминал
+flask run --debug
+ * Debug mode: on
+WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
+ * Running on http://127.0.0.1:5000
+Press CTRL+C to quit
+ * Restarting with stat
+ * Debugger is active!
+ * Debugger PIN: 106-994-678
+```
+
+Теперь мы можем обратиться к нашему бэкенду через API по адресу [`http://127.0.0.1:5000/cities/2020`](http://127.0.0.1:5000/cities/2020).
+
+### Сведения о городе по идентификатору
+
+Перейдём ко второму методу. Получим подробные сведения о городе по его идентификатору.
+
+```py title=app.py
+@app.route("/city/")
+def city_by_id(id):
+ start_time = time.time()
+ db = sqlite3.connect(DB_LOCATION)
+ db.row_factory = sqlite3.Row
+ cursor = db.execute("SELECT * FROM cities WHERE id = ?", (id,))
+ city = cursor.fetchone()
+ cursor.close()
+ db.close()
+ r = Response(
+ json.dumps(dict(city), ensure_ascii=False),
+ mimetype="text/json",
+ )
+ # print("--- %s seconds ---" % (time.time() - start_time))
+ return r
+```
+
+Проверим этот метод [`http://127.0.0.1:5000/city/1000`](http://127.0.0.1:5000/city/1000).
+
+## Разработка фронтенда
+
+### Подключение данных и CORS
+
+Наш бэкенд возвращает данные в формате GeoJSON, поэтому мы можем сразу подключить их в нашу карту.
+
+```js title=main.js
+map.on("load", () => {
+ map.addSource('cities', {
+ type: 'geojson',
+ data: "http://localhost:5000/cities/2020" // бэкенд должен быть запущен
+ });
+
+ map.addLayer({
+ 'id': 'cities-layer',
+ 'source': 'cities',
+ 'type': 'circle',
+ 'paint': {
+ 'circle-stroke-width': 1,
+ 'circle-stroke-color': '#FFFFFF',
+ // SELECT MIN(total_points), MAX(total_points) FROM cities
+ 'circle-color': [
+ 'interpolate',
+ ['linear'],
+ ['get', 'total_points'],
+ 50,
+ '#d7191c',
+ 150,
+ '#ffffbf',
+ 250,
+ '#1a9641'
+ ],
+ 'circle-opacity': 0.8,
+ // SELECT DISTINCT group_name FROM cities
+ 'circle-radius': [
+ "match",
+ ['get', 'group_name'],
+ 'Малый город', 3,
+ 'Средний город', 6,
+ 'Большой город', 6,
+ 'Крупный город', 8,
+ 'Крупнейший город', 12,
+ 0 // остальные
+ ]
+ }
+ });
+})
+```
+
+Однако на карте мы не увидим искомых городов. Чтобы узнать почему, проверим вкладку "Сеть" в инструментах разработчика. У запроса к списку городов мы увидим надпись **Ошибка CORS**.
+
+Механизм CORS -- Cross-Origin Resource Sharing -- призван повысить безопасность веб-страницы. Нам, чтобы избежать ошибки CORS, надо указать, что API может отвечать на запросы любых веб-страниц.
+
+> CORS -- это история про *веб-страницы*, поэтому выполняя запросы к API напрямую мы с ней не сталкивались.
+
+```diff lang=py title=app.py
+...
+@app.route("/cities/") # путь API, к которому обращается пользователь
+def cities_by_year(year): # функция, которая будет выполняться при обращении
+ ...
+ r = Response( # формируем ответ
+ json.dumps(geojson, ensure_ascii=False), # ensure_ascii=False, чтобы нормально отображалась кириллица
+ mimetype="text/json", # указываем тип данных
++ headers={"Access-Control-Allow-Origin": "*"}
+ )
+ # print("--- %s seconds ---" % (time.time() - start_time))
+ return r
+
+@app.route("/city/")
+def city_by_id(id):
+ ...
+ r = Response(
+ json.dumps(dict(city), ensure_ascii=False),
+ mimetype="text/json",
++ headers={"Access-Control-Allow-Origin": "*"}
+ )
+ # print("--- %s seconds ---" % (time.time() - start_time))
+ return r
+```
+
+После добавления заголовков о том, что API может обслуживать любые веб-страницы, в ответ мы получаем наш список городов в формате GeoJSON.
+
+### Выбор года
+
+Дадим пользователю возможность выбирать год, за который он хочет видеть индекс городов.
+
+Разметим элемент с выпадающем списком.
+
+```diff lang=html title=index.html
+
+
+
++
++
Год
++
++
+
+
+```
+
+Сделаем так, чтобы он выводился поверх карты.
+
+```diff lang=css title=style.css
+#map {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
++ z-index: -1;
+}
+```
+
+Запрограммируем реакцию карты на выбор года. Асинхронный запрос за нас выполнит картографическая библиотека.
+
+```js title=main.js
+map.on("load", () => {
+ ...
+ document.getElementById("year-selector").addEventListener(
+ 'change',
+ (e) => {
+ const year = e.target.value // фиксируем выбранный год
+ map.getSource('cities').setData(`http://localhost:5000/cities/${year}`) // меняем источник данных
+ }
+ )
+})
+```
+
+### Сведения о городе
+
+Выведем по клику на город подробные сведения о нём. Для отображения информации используем модальное окно, то есть диалоговое окно вспывающее поверх страницы.
+
+```diff lang=html title=index.html
+
+
+ ...
++
+
+
+```
+
+При клике будем выполнять запрос к методу API, который возвращает подробную информацию о городе. Здесь мы работаем с асинхронностью самостоятельно.
+
+```js title=main.js
+map.on("load", () => {
+ ...
+ map.on('click', 'cities-layer', (e) => {
+ // console.log(e.features[0].properties.id)
+ fetch(`http://localhost:5000/city/${e.features[0].properties.id}`)
+ .then(response => response.json())
+ .then(cityProperties => {
+ // console.log(cityProperties)
+ document.getElementById("city-details-modal").innerHTML = `
${cityProperties.name}
+
+
Численность населения
${cityProperties.people_count} тыс. чел
+
Индекс качества городской среды
${cityProperties.total_points} / 360
+
+
Жилье и прилегающие пространства
${cityProperties.house_points} / 60
+
Озелененные пространства
${cityProperties.park_points} / 60
+
Общественно-деловая инфраструктура
${cityProperties.business_points} / 60
+
Социально-досуговая инфраструктура
${cityProperties.social_points} / 60
+
Улично-дорожная
${cityProperties.street_points} / 60
+
Общегородское пространство
${cityProperties.common_points} / 60
`
+ document.getElementById("city-details-modal").showModal() // showModal() -- встроенный метод элемента