Blueprint: организуем большое приложение на Flask
Восьмой урок курса Flask: Blueprint — мини-приложения внутри одного проекта. Регистрируем блюпринты, задаём префиксы адресов, разносим шаблоны по папкам и рефакторим блог из одного файла в модули.
Редакция Питоники
Наш учебный блог незаметно разросся: маршруты постов, комментариев, поиска, админки — всё в одном app.py, и файл перевалил за тысячу строк. Ctrl+F по слову route находит 27 совпадений, и половина из них — не те, что нужны. Переименовали переменную в шаблоне постов — непонятно почему отвалилась форма входа. Знакомо? Тогда у Flask для вас есть подарок из коробки: Blueprint.
Блюпринт — это кусок приложения, упакованный в отдельный модуль: свои маршруты, свои шаблоны, при желании своя статика. Приложение собирается из блюпринтов, как из конструктора: auth отвечает за вход, blog — за посты, api — за данные. В этом уроке разрежем наш блог на модули и заодно разберём грабли, которые на этом пути расставлены щедро: молчаливые 404, конфликты имён эндпоинтов и потерянные шаблоны.
Проблема одного файла: когда app.py перестаёт помещаться в голове
Пока приложение маленькое, один файл — это честно и удобно. Но у монолита из одного файла есть симптомы, которые проявляются рано и болят долго:
- Прокрутка: чтобы добавить маршрут комментариев, листаешь мимо всех маршрутов постов и авторизации.
- Конфликты имён: view-функция comments уже занята, новую приходится называть comments_all.
- Страх рефакторинга: правка в одном месте непредсказуемо ломает другое.
- Слепые тесты: чтобы проверить один модуль, приходится поднимать всё приложение целиком.
Вот сокращённый в три раза портрет нашего app.py — три разные темы живут вперемешку:
from flask import Flask, render_template, request, session, jsonify
app = Flask(__name__)
@app.route("/")
def index():
return render_template("index.html")
@app.route("/posts")
def posts():
return render_template("posts.html")
@app.route("/posts/<int:post_id>")
def post_detail(post_id):
return render_template("post.html", post_id=post_id)
@app.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
session["user"] = request.form["username"]
return render_template("login.html")
@app.route("/api/posts")
def api_posts():
return jsonify([{"id": 1, "title": "Первый пост"}])
Каждый блок здесь тянет свой лист влево-вправо, и от этого код не становится хуже технически — он становится хуже для человека. Через месяц вы не вспомните, на какой строке живёт login. Время резать.
Что такое Blueprint: приложение в миниатюре
Blueprint — объект, который копит маршруты, шаблоны и статику, не привязываясь к приложению. Снаружи он почти неотличим от Flask(__name__): те же декораторы @bp.route, тот же render_template. Разница принципиальна одна: блюпринт сам по себе не запускается. Он оживает только после register_blueprint в основном приложении — тогда Flask переносит все его маршруты в общий роутер.
Имя объекта выбираете вы: чаще всего bp или название модуля. В конструктор Blueprint первым аргументом передают имя — оно станет префиксом для имён эндпоинтов, и к этому мы ещё вернёмся, потому что на именах спотыкаются чаще всего.
# blog.py
from flask import Blueprint
bp = Blueprint("blog", __name__)
@bp.route("/posts")
def posts():
return "Список постов"
@bp.route("/posts/<int:post_id>")
def post_detail(post_id):
return f"Пост номер {post_id}"
# app.py
from flask import Flask
from blog import bp as blog_bp
app = Flask(__name__)
app.register_blueprint(blog_bp)
@app.route("/")
def index():
return "Главная страница"
$ curl http://127.0.0.1:5000/posts
$ curl http://127.0.0.1:5000/posts/3
Список постов Пост номер 3
Почему это работает: отложенная регистрация
Декоратор @bp.route не вешает маршрут сразу. Он дописывает его во внутренний список блюпринта — отложено. Когда вы вызываете app.register_blueprint(blog_bp), Flask проходит по этому списку и добавляет каждый маршрут в общий роутер приложения. Отсюда приятное следствие: один и тот же блюпринт можно зарегистрировать в двух приложениях — например, в рабочем и в тестовом — и маршруты появятся в обоих.
url_prefix: адресная полка для каждого модуля
Пока маршруты блюпринта живут в корне сайта: /posts, /posts/3. Обычно модулю выделяют префикс — тогда все его адреса начинаются с него, и адресная структура сайта читается сама: /blog/... — посты, /auth/... — вход, /api/... — данные. Префикс задаётся одним аргументом:
# способ 1: префикс при регистрации - карта адресов видна в app.py
app.register_blueprint(blog_bp, url_prefix="/blog")
# способ 2: префикс прямо в конструкторе блюпринта
bp = Blueprint("blog", __name__, url_prefix="/blog")
Теперь список постов отвечает на /blog/posts, а старый адрес /posts отдаёт 404 Not Found — его больше нет. Полный адрес склеивается из префикса и маршрута внутри блюпринта: /blog плюс /posts. Об этом стоит помнить при рефакторинге: ссылки из писем и закладок пользователей по старым адресам сломаются, поэтому префикс лучше заводить сразу, а не когда проект уже индексируется поисковиками.
url_for с блюпринтами: имена эндпоинтов с точкой
У каждого маршрута во Flask есть имя эндпоинта — по умолчанию это имя view-функции. У блюпринтов к имени добавляется префикс: функция posts из блюпринта blog получает эндпоинт blog.posts. Именно это спасает от конфликтов: posts может спокойно жить и в blog, и в api — полные имена разные. В url_for и в шаблонах пишите имя целиком:
from flask import url_for
with app.test_request_context():
print(url_for("index"))
print(url_for("blog.posts"))
print(url_for("blog.post_detail", post_id=3))
/ /blog/posts /blog/posts/3
В шаблонах вы будете писать то же самое: {{ url_for('blog.post_detail', post_id=post.id) }}. Обратная сторона имён с точкой: напишете url_for("posts"), забыв префикс, — и получите ошибку построения адреса. Хорошая новость в том, что свежие версии werkzeug подсказывают правильное имя прямо в тексте ошибки:
url_for("posts")
Traceback (most recent call last):
...
werkzeug.routing.exceptions.BuildError: Could not build url for endpoint
'posts'. Did you mean 'blog.posts' instead?
Несколько блюпринтов и карта адресов
Реальное приложение — это не один блюпринт, а три-пять. Регистрируются они одинаково, просто по очереди:
app.register_blueprint(blog_bp, url_prefix="/blog")
app.register_blueprint(auth_bp) # без префикса
app.register_blueprint(api_bp, url_prefix="/api")
print(app.url_map)
Map([<Rule '/' (GET, HEAD, OPTIONS) -> index>,
<Rule '/blog/posts' (GET, HEAD, OPTIONS) -> blog.posts>,
<Rule '/login' (GET, HEAD, OPTIONS) -> auth.login>,
<Rule '/api/posts' (GET, HEAD, OPTIONS) -> api.posts>])Обратите внимание на круглые скобки в выводе: за каждым адресом стоит список разрешённых HTTP-методов и имя эндпоинта. auth я здесь зарегистрировал без префикса — тогда вход живёт на /login, привычном адресе всех сайтов. Выбор «префикс или нет» — это решение об адресах, которое стоит принимать осознанно: редизайн адресов позже стоит дороже, чем минута размышлений сейчас.
Шаблоны и статика блюпринта: свои папки
У блюпринта могут быть собственные папки шаблонов и статики — они задаются прямо в конструкторе:
bp = Blueprint(
"blog",
__name__,
template_folder="templates",
static_folder="static",
)
Теперь Flask ищет шаблон сначала в папке приложения (templates/), потом в папке блюпринта (blog/templates/). Из этого порядка вырастает правило, которое экономит часы отладки: шаблоны блюпринта кладите в подпапку с его именем — blog/templates/blog/post.html, и рендерьте как render_template("blog/post.html"). Если два модуля обзаведутся шаблонами с одинаковым именем post.html, приложение молча возьмёт первый попавшийся — а ошибку вы заметите, когда на странице постов отрисуется форма входа.
Синтаксис самих шаблонов не меняется — переменные, циклы и наследование base.html работают как в уроке про шаблоны Jinja2, просто файлы теперь лежат по своим папкам.
Рефакторинг блога: проект до и после
Собираем всё вместе. Вот структура проекта после разрезания на модули:
myblog/
app.py # Flask + register_blueprint
blog/
__init__.py
routes.py # Blueprint("blog")
templates/
blog/
posts.html
post.html
auth/
__init__.py
routes.py # Blueprint("auth")
templates/
auth/
login.html
templates/
base.html # общий каркас
index.html
static/
style.css
Порядок действий при таком рефакторинге я бы предложил такой:
- Создайте пакет модуля: папку blog с файлами __init__.py и routes.py.
- Перенесите маршруты в routes.py, замените @app.route на @bp.route и объявите Blueprint.
- Зарегистрируйте блюпринт в app.py: app.register_blueprint(blog_bp, url_prefix="/blog").
- Разложите шаблоны по подпапкам и поправьте render_template: путь теперь blog/post.html.
- Проверьте url_for: каждое имя эндпоинта получило префикс blog. — включая ссылки в base.html.
Самый коварный пункт — последний: url_for спрятаны в шаблонах, и BuildError вылезет только на той странице, где он есть. Пройдитесь по сайту руками или держите под рукой общий каркас base.html — обычно именно в нём живёт главное меню со ссылками на все модули. Подробности про общие шаблоны и статические файлы мы разбирали в уроке про структуру проекта.
Типичные ошибки при работе с Blueprint
Ещё одна ловушка из серии «работает, но не так»: циклический импорт. Если blog/routes.py импортирует что-то из app.py, а app.py импортирует блюпринт из blog — Python зациклится с ошибкой ImportError: cannot import name. Лечится разделением: объявление приложения, модели и блюпринты живут в разных файлах, и зависимости идут строго в одну сторону. В больших проектах блюпринты выносят в отдельный пакет с собственным __init__.py, где и создаётся Blueprint.
Когда блюпринт не нужен
Честности ради: если у вас пять маршрутов и один шаблон, блюпринты — оверинжиниринг. Один app.py читается быстрее, чем дерево из папок. Сигналы к разделению: файл перевалил за 300–400 строк, имена функций начали конфликтовать, появился второй смысловой блок (например, к блогу добавили API), или вы хотите переиспользовать модуль в другом проекте. Между «даже не думай» и «рези немедленно» — целая серая зона, и решение в ней всегда за вами: моё правило — резать тогда, когда страница app.py перестала помещаться на экране целиком.
Итоги: из монстра — в конструктор
Теперь у вас есть всё для проекта любого размера: Blueprint объявляется в своём модуле, регистрируется через register_blueprint, адреса модуля собираются под общим url_prefix, а url_for работает с именами вида blog.posts. Шаблоны и статика живут в папках модулей, общие — в корне. Снаружи приложение то же самое, внутри — порядок.
Следующий логичный шаг — самый настоящий модуль auth: в уроке 9 мы построим регистрацию и вход на Flask: модель User, хеширование паролей через werkzeug и декоратор login_required для защиты страниц блога. Он собран на Blueprint из этого урока — так что код auth ляжет в структуру, которую вы только что выстроили. До финала остался один шаг; карта всего курса — самоучитель Flask.
Сначала предскажи ответ в голове — это главный навык программиста.
registered = []
def add_route(target, path):
target.append(path)
app_routes = []
bp_routes = []
add_route(bp_routes, "/posts")
add_route(bp_routes, "/posts/<int:post_id>")
# блюпринт пока НЕ зарегистрирован в приложении
print(len(app_routes), len(bp_routes))
routes = {
"index": "/",
"blog.posts": "/blog/posts",
"blog.post_detail": "/blog/posts/<int:post_id>",
}
def url_for(endpoint, **kwargs):
url = routes[endpoint]
for key, value in kwargs.items():
url = url.replace("<int:" + key + ">", str(value))
return url
print(url_for("blog.post_detail", post_id=7))
1. Что такое Blueprint во Flask?
2. Где можно задать url_prefix для блюпринта?
3. Какое имя эндпоинта у функции posts из блюпринта blog?
4. Что произойдёт, если забыть вызвать app.register_blueprint?
5. Почему шаблоны блюпринта кладут в подпапку blog/templates/blog/?
Соберите мини-url_for, как во Flask: словарь PREFIXES хранит префиксы блюпринтов (blog -> /blog, auth -> /auth), а ROUTES — шаблоны маршрутов относительно префикса (blog.post_detail -> post_detail/<int:post_id>, auth.login -> login). Функция my_url(endpoint, **kwargs) должна собрать адрес из префикса и маршрута, затем подставить параметры в плейсхолдеры вида <int:post_id>. Выведите два адреса.
Как посмотреть все маршруты Flask-приложения?
Распечатайте app.url_map: в выводе — все правила приложения с разрешёнными методами и полными именами эндпоинтов, например <Rule '/blog/posts' (GET, HEAD, OPTIONS) -> blog.posts>. Это первый помощник, когда url_for не находит эндпоинт и падает с BuildError.
Что такое Blueprint во Flask и зачем он нужен?
Blueprint — это способ вынести часть приложения (маршруты, шаблоны, статику) в отдельный модуль и зарегистрировать её в основном приложении. Блюпринты решают проблему разросшегося app.py: модули auth, blog и api живут в разных файлах и не конфликтуют именами.
Как разбить Flask-приложение на модули?
Для каждой логической части создайте пакет с файлом routes.py, объявите в нём Blueprint и перенесите маршруты, заменив @app.route на @bp.route. В app.py зарегистрируйте блюпринты с url_prefix, разложите шаблоны по подпапкам и добавьте префиксы к именам эндпоинтов в url_for.
Чем Blueprint отличается от отдельного приложения Flask?
Блюпринт нельзя запустить: у него нет app.run() и собственного сервера. Он получает маршруты, шаблоны и конфигурацию только через родительское приложение. Зато один блюпринт можно подключить к нескольким приложениям — например, к рабочему и к тестовому.
Почему url_for не находит функцию из блюпринта?
У маршрутов блюпринта имена с префиксом: не posts, а blog.posts. Ошибка BuildError с подсказкой «Did you mean 'blog.posts' instead?» значит, что вы написали короткое имя. Распечатайте app.url_map — там перечислены все эндпоинты приложения.
Понравился урок? Сошлитесь на него
«Блюпринт — это кусок приложения, упакованный в отдельный модуль: свои маршруты, свои шаблоны, при желании своя статика.»
Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.
Что читать дальше
Flask · Урок 3
Шаблоны Jinja2: HTML-страницы с данными из Python
Третий урок курса Flask: шаблоны Jinja2 — переменные, циклы, фильтры и наследование base.html. Отличие этого урока: все примеры Jinja2 можно отрендерить прямо на странице.
Flask · Урок 7
Структура проекта и статические файлы во Flask: static и url_for
Седьмой урок курса Flask: каноническая структура проекта, папка static, url_for, подключение CSS и Bootstrap, favicon и настройки в app.config.
Flask · Урок 9
Регистрация и вход: аутентификация на Flask
Девятый урок курса Flask: модель User, хеширование паролей, маршруты register/login/logout и декоратор login_required. Демо хеширования sha256 с солью запускается прямо на странице.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
aiogram · Урок 10
Проект: телеграм-бот трекер расходов на aiogram с базой данных
Сквозной проект курса: бот-трекер расходов с inline-кнопками, FSM-диалогом, SQLite-хранилищем и отчётом за месяц. Рабочая версия в песочнице, полный код на aiogram и чек-лист запуска.
телеграм бот python проектaiogram проект
FastAPI · Урок 10
Проект: REST API сервиса заметок с базой данных
Сквозной проект курса: сервис заметок с тегами, поиском, SQLite и JWT. Структура по файлам, контракты Pydantic, тесты и README — портфолио-проект за пару вечеров.
fastapi проектструктура проекта fastapi
Flask · Урок 20
Проект: сервис заметок с тестами — фабрика, blueprint и pytest
Финал расширения: собираем всё выученное в одно приложение — JSON API заметок на фабрике create_app с блюпринтом, дымовой прогон через test_client и полный pytest-набор на 200, 201, 400 и 404.
flask проект api заметок тестыflask api заметок пример
Проверьте знания по Flask
В челлендже — 20 задач по Flask, по 2 из каждого урока этого раздела. Формат: фрагмент кода и четыре варианта — что напечатает. После ответа — вердикт и объяснение со ссылкой на урок-источник.
Тест по Flask: 20 задач