Веб-приложение — это программа, с которой пользователь работает через браузер: интерфейс загружается по сети, а данные и логика в основном живут на сервере. От обычного сайта его отличает задача, а не технология: сайт в первую очередь показывает информацию, веб-приложение обрабатывает действия пользователя и хранит состояние (заказы, заметки, аккаунты). Граница размыта — интернет-магазин одновременно и сайт, и приложение.
Содержание
Три слоя, о которых дальше пойдет речь: фронтенд (HTML, CSS и JavaScript в браузере), бэкенд (программа на сервере, которая принимает HTTP-запросы и применяет правила) и база данных (хранилище, к которому обращается только бэкенд). Ниже — план в шесть шагов: требования, архитектура, стек, рабочий пример, безопасность, деплой.
Шаг 1. Требования и MVP
До кода нужно ответить на вопросы, которые потом дорого менять. Кто пользователи и нужен ли вход. Какие данные храним и есть ли среди них персональные. Сколько пользователей ожидаем в первые месяцы. Нужна ли работа с телефона и офлайн.
Сценарии удобно записывать коротко: «как покупатель, я хочу добавить товар в корзину, чтобы оплатить позже». Из них выбирают MVP — минимальный набор, без которого продукт не имеет смысла; остальное откладывают. Если приложение обрабатывает персональные данные граждан РФ, к нему применимы требования 152-ФЗ — это влияет на хостинг и доступы, поэтому решается здесь, а не после запуска.
Шаг 2. Архитектура
Базовая модель общая: браузер отправляет HTTP-запрос, бэкенд проверяет его, читает или меняет данные в базе и возвращает HTML-страницу или JSON. Варианты различаются тем, где собирается интерфейс.
| Подход | Где собирается страница | Когда подходит |
|---|---|---|
| MPA (многостраничное) | На сервере, каждый переход — новая страница | Магазины, контентные проекты, админки; проще SEO |
| SPA (одностраничное) | В браузере, данные приходят через API | Кабинеты и редакторы с множеством действий без перезагрузки |
| SSR/гибрид (Next.js, Nuxt) | Первая загрузка на сервере, дальше как SPA | Нужны и SEO, и сложный интерфейс |
PWA — не четвертый тип, а надстройка (манифест и service worker) над SPA или MPA: установка на главный экран, офлайн, push-уведомления, набор возможностей зависит от браузера и ОС. И SPA сама по себе не менее безопасна, чем MPA: уязвимости появляются в коде бэкенда и API, а не от числа страниц.
Шаг 3. Выбор стека
Стек выбирают по задаче и команде, а не по рейтингам. Фронтенд в любом случае опирается на HTML, CSS и JavaScript (или TypeScript).
| Задача | Что взять | Когда иначе |
|---|---|---|
| Понять механику | Python или Node.js без фреймворка, SQLite | Появились вход и роли — нужен фреймворк |
| Продукт с формами и админкой | Django, Laravel, Ruby on Rails | Команда пишет на другом языке — фреймворк ее языка |
| API для SPA или мобильного клиента | FastAPI, Express/NestJS, Spring Boot, ASP.NET Core | Для простого CRUD часто хватает MPA |
| Данные | PostgreSQL или MySQL, SQLite для прототипа | Документная база — когда структура записей правда разная |
Конструкторы (no-code) закрывают лендинги и простые магазины. Когда нужна своя логика, интеграции или контроль над данными, нужен код.
Шаг 4. Минимальный рабочий пример
Все три слоя в одном файле — учебное приложение «Заметки» на стандартной библиотеке Python: форма, сервер, база SQLite. Ничего устанавливать не нужно. Проверено на Python 3.14:
import html
import sqlite3
from pathlib import Path
from urllib.parse import parse_qs
from wsgiref.simple_server import make_server
DB_PATH = Path(__file__).parent / "var" / "notes.sqlite"
MAX_LEN = 200
def db():
DB_PATH.parent.mkdir(exist_ok=True)
conn = sqlite3.connect(DB_PATH)
conn.execute("CREATE TABLE IF NOT EXISTS notes ("
"id INTEGER PRIMARY KEY, "
"text TEXT NOT NULL CHECK (length(text) BETWEEN 1 AND 200))")
return conn
def index(environ):
with db() as conn:
rows = conn.execute("SELECT text FROM notes ORDER BY id").fetchall()
items = "".join(f"<li>{html.escape(text)}</li>" for (text,) in rows)
page = ("<!doctype html><meta charset='utf-8'><title>Заметки</title>"
"<form method='post' action='/notes'>"
"<input name='text' maxlength='200' required> <button>Добавить</button>"
f"</form><ul>{items}</ul>")
return "200 OK", [("Content-Type", "text/html; charset=utf-8")], page
def add_note(environ):
size = int(environ.get("CONTENT_LENGTH") or 0)
if size > 4096:
return "413 Content Too Large", [], "Слишком большой запрос"
form = parse_qs(environ["wsgi.input"].read(size).decode("utf-8"))
text = form.get("text", [""])[0].strip()
if not 1 <= len(text) <= MAX_LEN:
return "400 Bad Request", [], "Текст заметки: от 1 до 200 символов"
with db() as conn:
conn.execute("INSERT INTO notes (text) VALUES (?)", (text,))
return "303 See Other", [("Location", "/")], ""
ROUTES = {("GET", "/"): index, ("POST", "/notes"): add_note}
def app(environ, start_response):
handler = ROUTES.get((environ["REQUEST_METHOD"], environ["PATH_INFO"]))
if handler is None:
status, headers, body = "404 Not Found", [], "Не найдено"
else:
status, headers, body = handler(environ)
headers = headers or [("Content-Type", "text/plain; charset=utf-8")]
start_response(status, headers)
return [body.encode("utf-8")]
if __name__ == "__main__":
with make_server("127.0.0.1", 8000, app) as server:
print("Открой http://127.0.0.1:8000")
server.serve_forever()
Запустите python3 app.py и откройте http://127.0.0.1:8000 — появится поле ввода с кнопкой «Добавить» и список заметок под ними. Проверка из второго терминала:
curl -s -o /dev/null -w "%{http_code}\n" -d "text=Купить молоко" http://127.0.0.1:8000/notes
curl -s -o /dev/null -w "%{http_code}\n" --data-urlencode "text=<b>жирный?</b>" http://127.0.0.1:8000/notes
curl -s -w " [%{http_code}]\n" -d "text=" http://127.0.0.1:8000/notes
curl -s http://127.0.0.1:8000/; echo
curl -s -w " [%{http_code}]\n" http://127.0.0.1:8000/var/notes.sqlite
Фактический вывод:
303
303
Текст заметки: от 1 до 200 символов [400]
<!doctype html><meta charset='utf-8'><title>Заметки</title><form method='post' action='/notes'><input name='text' maxlength='200' required> <button>Добавить</button></form><ul><li>Купить молоко</li><li><b>жирный?</b></li></ul>
Не найдено [404]
Как это читать по слоям:
- Фронтенд — HTML-форма.
requiredиmaxlengthпомогают пользователю, но не защищают: curl отправил пустой текст в обход браузера. Поэтому длину проверяют сервер (ответ 400) и база (CHECK). - Бэкенд — функция
appи таблица маршрутов. После успешного POST сервер отвечает 303 с переходом на главную (Post/Redirect/Get), и обновление страницы не отправит форму повторно. - Данные — база лежит в
var/, а сервер отдает только описанные маршруты, поэтому запрос к файлу базы получает 404.
В примере три разных защитных действия, и они не заменяют друг друга: валидация входа (длина), кодирование вывода под HTML (html.escape превратил тег <b> в текст) и параметризация SQL (значение передается через ?).
Ошибка: SQL, собранный строкой
import sqlite3
conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE notes (id INTEGER PRIMARY KEY, text TEXT NOT NULL)")
text = "It's done"
conn.execute(f"INSERT INTO notes (text) VALUES ('{text}')")
Результат на Python 3.14:
sqlite3.OperationalError: near "s": syntax error
Апостроф закрыл строку раньше времени: безобидный текст ломает запрос, а подобранный может изменить его смысл — это SQL-инъекция. Исправление — параметр: conn.execute("INSERT INTO notes (text) VALUES (?)", (text,)), после чего выборка вернет [("It's done",)].
Шаг 5. Безопасность: базовый минимум
Пример выше учебный и закрывает только часть рисков. Ориентир для веб-приложений — список OWASP Top 10: нарушения контроля доступа, инъекции, ошибки конфигурации, уязвимые зависимости, слабая аутентификация и другие классы проблем.
| Прием | От чего защищает | Чего не делает |
|---|---|---|
| Параметризованные запросы | SQL-инъекции через значения | Не проверяет права на запись |
| Кодирование вывода | XSS через сохраненные данные | Не заменяет валидацию входа |
| Проверка прав на каждом запросе | Доступ к чужим данным по подмененному id | Не защищает от кражи сессии |
| HTTPS | Перехват и подмену трафика | Не защищает сервер и базу |
| Хеширование паролей (Argon2id) | Раскрытие паролей при утечке базы | Не защищает вход от подбора — нужны лимиты попыток |
До продакшена нужны еще: аутентификация и CSRF-токены, как только появляется вход; лимиты частоты запросов; секреты в переменных окружения, а не в репозитории; журнал ошибок без паролей и персональных данных; обновление зависимостей. Встроенный wsgiref — сервер для разработки, в продакшене приложение работает под сервером приложений за обратным прокси.
Шаг 6. Git, тесты и деплой
Код хранят в Git с первого дня, а ключевые сценарии (заметка добавляется, пустая отклоняется) покрывают тестами, которые запускаются перед каждой выкладкой. Порядок первого деплоя:
- Площадка: PaaS или небольшой VPS с Linux.
- Настройки — в переменные окружения: адрес базы, секретный ключ, режим отладки выключен.
- Продакшен-база (PostgreSQL или MySQL) и миграции схемы.
- Сервер приложений (для Python — например, Gunicorn или Uvicorn) за обратным прокси с HTTPS.
- Проверка снаружи: главная отвечает 200, форма работает, служебные файлы отдают 404.
- Резервные копии базы с проверкой восстановления и мониторинг доступности.
Дорожная карта до своего приложения
- HTML и CSS: страница с формой и списком.
- JavaScript: события и запросы к серверу через
fetch. - Один серверный язык и HTTP: методы, коды ответов, заголовки, cookie.
- SQL и реляционная СУБД: таблицы, ограничения, JOIN, индексы.
- Фреймворк своего языка: маршруты, шаблоны, ORM, аутентификация.
- Git, тесты, деплой с HTTPS.
Артефакт в конце — приложение с регистрацией, хранением данных и публичным адресом, код которого лежит в репозитории. У новичка путь обычно занимает месяцы, а не недели, в зависимости от часов в неделю.
Если не получилось
| Симптом | Частая причина | Что сделать |
|---|---|---|
Address already in use |
Порт 8000 занят | Остановить старый процесс или сменить порт |
| С другого устройства не открывается | Сервер слушает 127.0.0.1 |
0.0.0.0 — только в доверенной сети |
| Ошибка CORS в браузере | Фронтенд и API на разных доменах или портах | Разрешить нужный источник, не * для запросов с cookie |
| SPA: 404 при обновлении страницы | Сервер не знает клиентских маршрутов | Отдавать index.html для маршрутов приложения |
| После деплоя 500 | Нет переменных окружения или миграций | Журнал ошибок сервера, сверка настроек |
Выводы
- Веб-приложение отличается от сайта задачей: обрабатывает действия пользователя и хранит состояние.
- Начинать нужно с требований и MVP: пользователи, данные и нагрузка определяют архитектуру и хостинг.
- MPA, SPA и SSR различаются местом сборки интерфейса; PWA — надстройка над ними.
- Валидация входа, кодирование вывода и параметризация SQL — три разных действия; учебный пример с ними еще не готов к продакшену.
- Деплой — это переменные окружения, продакшен-база, сервер приложений за HTTPS, резервные копии и проверка снаружи.
Где применяется / связь с практикой
Освойте тему на практике
Такой план — ежедневная работа fullstack-разработчика: одному человеку нужно держать в голове интерфейс, API, схему базы и выкладку. Если хочется пройти путь от формы до задеплоенного приложения системно, с ревью кода и проектами в портфолио, посмотрите специализацию Fullstack Developer. Отдельные темы — архитектуру, фреймворки, безопасность — удобно попробовать на бесплатных открытых уроках Otus.
FAQ
Можно ли написать веб-приложение только на JavaScript?
Да: в браузере работает JavaScript, на сервере — Node.js, поэтому фронтенд и бэкенд пишут на одном языке. База данных при этом все равно отдельная.
Нужен ли фреймворк для первого проекта?
Один раз собрать приложение без фреймворка полезно для понимания механики. Для продукта с входом и ролями фреймворк экономит время и закрывает типовые задачи безопасности.
Чем веб-приложение отличается от мобильного?
Веб-приложение открывается в браузере и обновляется на сервере без установки, мобильное ставится из магазина и получает больше доступа к устройству. PWA частично сокращает разрыв, но возможности зависят от платформы.



