Как написать веб-приложение: план от требований до деплоя

Как написать веб-приложение: план от требований до деплоя Полезное

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

Три слоя, о которых дальше пойдет речь: фронтенд (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>&lt;b&gt;жирный?&lt;/b&gt;</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 с первого дня, а ключевые сценарии (заметка добавляется, пустая отклоняется) покрывают тестами, которые запускаются перед каждой выкладкой. Порядок первого деплоя:

  1. Площадка: PaaS или небольшой VPS с Linux.
  2. Настройки — в переменные окружения: адрес базы, секретный ключ, режим отладки выключен.
  3. Продакшен-база (PostgreSQL или MySQL) и миграции схемы.
  4. Сервер приложений (для Python — например, Gunicorn или Uvicorn) за обратным прокси с HTTPS.
  5. Проверка снаружи: главная отвечает 200, форма работает, служебные файлы отдают 404.
  6. Резервные копии базы с проверкой восстановления и мониторинг доступности.

Дорожная карта до своего приложения

  1. HTML и CSS: страница с формой и списком.
  2. JavaScript: события и запросы к серверу через fetch.
  3. Один серверный язык и HTTP: методы, коды ответов, заголовки, cookie.
  4. SQL и реляционная СУБД: таблицы, ограничения, JOIN, индексы.
  5. Фреймворк своего языка: маршруты, шаблоны, ORM, аутентификация.
  6. 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 частично сокращает разрыв, но возможности зависят от платформы.

OTUS Журнал
Скидка 5% 14-20 сентября на курсы (popup)