Проект: блог на Flask с постами и комментариями
Финальный проект курса: блог на Flask с постами, комментариями и пагинацией. Модели, CRUD-маршруты, наследование шаблонов, деплой на PythonAnywhere и разбор каждого решения.
Редакция Питоники
Резюме отправлено, вас позвали на собеседование и попросили показать живой проект. Ноутбуки и упражнения не в счёт: нужен адрес, по которому открывается настоящий сайт. Хорошая новость: у вас уже есть всё, чтобы собрать такой сайт за вечер, — девять уроков позади. Плохая тоже есть: по отдельности кусочки работают, а вместе их вы ещё не соединяли. Этим и займёмся: сделаем блог на Flask с постами и комментариями — классический первый проект, который не стыдно показать.
Проект небольшой, но честный: список постов с пагинацией, страница поста с комментариями, создание, правка и удаление записей, общий каркас шаблонов и в конце — деплой. Кода в этом уроке много: он и есть проект. Зато каждое решение снабжено объяснением, почему выбран именно этот путь. А шаблоны, как и в уроке про Jinja2, можно отрендерить прямо на странице — интерактив здесь даже важнее, потому что собирать вы всё будете сами.
Что собираем: карта блога
Начнём с карты, чтобы не заблудиться в собственном проекте. Весь блог — это пять маршрутов, у каждого понятная роль:
| Маршрут | Методы | Что делает |
|---|---|---|
| / | GET | список постов, 5 на страницу, пагинация |
| /post/<int:post_id> | GET, POST | просмотр поста и форма комментария |
| /create | GET, POST | форма нового поста |
| /post/<int:post_id>/edit | GET, POST | правка поста |
| /post/<int:post_id>/delete | POST | удаление поста |
Заметьте, чего в таблице нет: регистрации, профиля, панели администратора. Это осознанное решение. Первый проект должен заработать целиком, а не на три четверти. Авторизацию, которую вы сделали в уроке 9, прикрутите следующим шагом — чек-лист в конце урока напомнит. Сейчас важнее провести одну запись по всему циклу жизни: создать, показать, отредактировать, удалить.
Структура проекта
Блог помещается в два Python-файла и папку шаблонов — та самая структура из урока про статику и организацию проекта. Blueprint'ы здесь не нужны: шесть маршрутов не заслуживают отдельных модулей. Когда маршрутов станет двадцать — разделите, а пока честность важнее архитектуры:
myblog/
app.py # маршруты и конфигурация
models.py # модели Post и Comment
seed.py # разовое наполнение базы тестовыми данными
requirements.txt # flask, flask-sqlalchemy
blog.db # база SQLite, создаёт seed.py
templates/
base.html
post_list.html
post_detail.html
post_form.html
404.html
static/
style.css
Модели Post и Comment: один-ко-многим
Сердце блога — две связанные модели. У одного поста много комментариев: это связь один-ко-многим, вы уже писали такие в уроке про базу данных. В базе её держит внешний ключ comment.post_id, а в коде — relationship, который даёт писать post.comments и comment.post, не вспоминая про SQL:
from datetime import datetime
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(120), nullable=False)
body = db.Column(db.Text, nullable=False)
created = db.Column(db.DateTime, default=datetime.now, index=True)
comments = db.relationship(
"Comment",
backref="post",
cascade="all, delete-orphan",
order_by="Comment.created",
)
def __repr__(self):
return f"<Post {self.id}: {self.title}>"
class Comment(db.Model):
id = db.Column(db.Integer, primary_key=True)
post_id = db.Column(db.Integer, db.ForeignKey("post.id"), nullable=False)
author = db.Column(db.String(60), nullable=False)
text = db.Column(db.Text, nullable=False)
created = db.Column(db.DateTime, default=datetime.now)
Разбор по строчкам, потому что здесь спрятано три решения. Заголовок — String(120), а не Text: короткое поле честно ограничивает автора и не даёт вставить в него статью. Тело — Text без ограничения. Поле created с default=datetime.now заполняется само при вставке, а index=True — не украшение: главная страница сортирует посты по дате при каждом запросе, и индекс делает это быстро даже на десятках тысяч записей.
Почему cascade важен для комментариев
cascade="all, delete-orphan" — страховка от комментариев-сирот. Без него удаление поста оставляло бы в базе строки с мёртвым post_id: код не упадёт, но на странице поста №999 вдруг показались бы комментарии к несуществующей записи. С cascade SQLAlchemy удаляет комментарии вместе с постом одной операцией — а аргумент backref="post" в это же время даёт каждому комментарию ссылку на родителя, чтобы в шаблоне комментария можно было написать comment.post.title. Аргумент order_by выстраивает комментарии по времени без ручной сортировки в каждом маршруте.
Создаём таблицы и наполняем блог
Пустой блог не покажешь ни работодателю, ни самому себе. Наполним его отдельным скриптом seed.py — запустите один раз, и в базе появится несколько постов с комментариями. Обязательная деталь: всё обращение к базе — внутри with app.app_context(). Без контекста SQLAlchemy не знает, к какому приложению относится запрос, и падает с RuntimeError: Working outside of application context.
from app import app
from models import db, Post, Comment
with app.app_context():
db.create_all()
p1 = Post(title="Зачем я завёл блог", body="Первая запись: мотивация и план.")
p2 = Post(title="Flask за выходные", body="Как я собрал первый сайт за два дня.")
db.session.add_all([p1, p2])
db.session.commit()
db.session.add(Comment(post_id=p1.id, author="Марина", text="Жду продолжения!"))
db.session.commit()
print("Постов:", Post.query.count())
print("Комментариев:", Comment.query.count())
print(p1)
print("Авторы комментариев:", [c.author for c in p1.comments])
Постов: 2 Комментариев: 1 <Post 1: Зачем я завёл блог> Авторы комментариев: ['Марина']
Последняя строка — маленький, но важный тест: p1.comments вернул список объектов Comment, значит relationship работает в обе стороны, и в шаблонах можно рассчитывать на пост.comments без ручных запросов.
Главная страница: список постов с пагинацией
Пока постов двенадцать, можно выводить все. Двести — уже нельзя: страница грузится вечность, читатель не доезжает до конца. Поэтому главная страница блога сразу делается с пагинацией. Вот каркас app.py вместе с первым маршрутом:
from flask import Flask, render_template, request, redirect, url_for, flash
from models import db, Post, Comment
app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///blog.db"
app.config["SECRET_KEY"] = "dev-secret-key"
db.init_app(app)
POSTS_PER_PAGE = 5
@app.route("/")
def index():
page = request.args.get("page", 1, type=int)
pagination = Post.query.order_by(
Post.created.desc()
).paginate(page=page, per_page=POSTS_PER_PAGE, error_out=False)
return render_template("post_list.html", pagination=pagination)
Строка request.args.get("page", 1, type=int) таит ловушку. Аргумент type=int не украшение: без него page будет строкой, и кто-то, открыв /?page=abc, получит ValueError: invalid literal for int() with base 10: 'abc' — и страницу с ошибкой вместо списка. С type=int Flask сам вернёт значение по умолчанию, то есть 1, для любого мусора в адресе. Я один раз ловил эту ошибку уже после выкладки сайта: в отладке я заходил по нормальным ссылкам, а роботы поисковика щупали всё подряд.
$ curl http://127.0.0.1:5000/
<header><a href="/">Все посты</a> | <a href="/create">Написать</a></header> <article> <h2>Flask за выходные</h2> <p>Как я собрал первый сайт за два дня.</p> </article> <nav> <span>Стр. 1 из 3</span> <a href="/?page=2">Вперёд</a> </nav>
Объект pagination — это Pagination из Flask-SQLAlchemy. В шаблон уходит именно он, а не список постов: у объекта есть и посты текущей страницы, и данные для навигации. Самые нужные атрибуты:
| Атрибут | Что содержит |
|---|---|
| pagination.items | посты текущей страницы — обычный список |
| pagination.page / pagination.pages | текущая страница и всего страниц |
| pagination.total | всего постов в базе |
| pagination.has_prev / has_next | есть ли предыдущая и следующая |
| pagination.prev_num / next_num | их номера (None, если страницы нет) |
Что выбрать: 404 или пустая страница
У paginate есть параметр error_out, и по умолчанию он True: запрос /?page=999 тогда отдаёт 404 — страницы ведь не существует. Я поставил error_out=False: на несуществующем номере страница просто отрисуется с пустым списком. Для блога я считаю это дружелюбнее — человек нажал на устаревшую ссылку, а ему молча показали главную без постов. Для API я бы выбрал наоборот: жёсткий 404 сразу говорит клиенту, что он ошибся адресом.
Страница поста: просмотр и приём комментариев
Один маршрут обслуживает два метода: GET рисует пост с комментариями, POST принимает новый комментарий. Вход в маршрут — db.get_or_404: если поста с таким id нет, посетитель получает честную страницу 404, а не ошибку базы:
@app.route("/post/<int:post_id>", methods=["GET", "POST"])
def post_detail(post_id):
post = db.get_or_404(Post, post_id)
if request.method == "POST":
author = request.form.get("author", "").strip()
text = request.form.get("text", "").strip()
if not author or not text:
flash("Заполните имя и текст комментария")
else:
comment = Comment(post_id=post.id, author=author, text=text)
db.session.add(comment)
db.session.commit()
return redirect(url_for("post_detail", post_id=post.id))
return render_template("post_detail.html", post=post)
Валидация намеренно минимальная — два поля не пустые. Но она уже на месте, и это главное: пустые комментарии — первая атака на любой блог, раньше ботов приходят только они. Экранирование ввода берёт на себя Jinja2 с автоэкранированием: даже если кто-то впишет в имя script-тег, на странице он останется безобидным текстом — тот самый механизм, что мы разбирали на примере XSS в уроке про шаблоны. Главное — не открыть дыру фильтром |safe.
POST-redirect-GET: зачем редирект после формы
Посмотрите на return redirect(...): мы не рисуем страницу после POST, а отправляем браузеру код 302 и просим снова зайти по адресу поста, уже методом GET. Приём называется POST-redirect-GET, и без него обновление страницы (F5) отправляло бы комментарий повторно — браузер честно спрашивает «повторить отправку данных?». Проверьте ответ сервера сами:
$ curl -i -X POST http://127.0.0.1:5000/post/1 -d "author=Марина" -d "text=Отличный пост!"
HTTP/1.1 302 FOUND Location: http://127.0.0.1:5000/post/1
После 302 браузер делает GET на тот же адрес — и видит страницу уже с новым комментарием. Флеш-сообщение «Заполните имя и текст» при этом показывается один раз и стирается из сессии, как мы разбирали в уроке про сессии и flash.
CRUD постов: создать, отредактировать, удалить
Три маршрута, и по привычному шаблону: GET показывает форму, POST сохраняет. Один шаблон post_form.html обслуживает и создание, и правку — в первом случае в него уходит post=None, во втором существующий пост:
@app.route("/create", methods=["GET", "POST"])
def create_post():
if request.method == "POST":
title = request.form.get("title", "").strip()
body = request.form.get("body", "").strip()
if not title or not body:
flash("Нужны и заголовок, и текст поста")
else:
post = Post(title=title, body=body)
db.session.add(post)
db.session.commit()
flash("Пост опубликован")
return redirect(url_for("post_detail", post_id=post.id))
return render_template("post_form.html", post=None)
@app.route("/post/<int:post_id>/edit", methods=["GET", "POST"])
def edit_post(post_id):
post = db.get_or_404(Post, post_id)
if request.method == "POST":
title = request.form.get("title", "").strip()
body = request.form.get("body", "").strip()
if title:
post.title = title
if body:
post.body = body
db.session.commit()
flash("Изменения сохранены")
return redirect(url_for("post_detail", post_id=post.id))
return render_template("post_form.html", post=post)
@app.route("/post/<int:post_id>/delete", methods=["POST"])
def delete_post(post_id):
post = db.get_or_404(Post, post_id)
db.session.delete(post)
db.session.commit()
flash("Пост удалён вместе с комментариями")
return redirect(url_for("index"))
В edit_post пустое поле не затирает старое значение: пользователь мог заполнить только заголовок. Удаление — единственный маршрут без GET: он принимает только POST. Это не перестраховка, это шрам истории веба:
Шаблоны: живой рендер каркаса и списка
Теперь разметка. В Flask шаблоны берутся из папки templates, а их место в этом уроке занимает словарь — чтобы вы могли запустить рендер прямо здесь. Логика та же, что в уроке про наследование: каркас base.html, дочерний post_list.html заполняет его блоки. Запускайте:
from jinja2 import Environment, DictLoader
templates = {
"base.html": """<!doctype html>
<html lang="ru">
<head>
<title>{% block title %}Блог про Flask{% endblock %}</title>
</head>
<body>
<header><a href="/">Все посты</a> | <a href="/create">Написать</a></header>
{% block content %}{% endblock %}
</body>
</html>""",
"post_list.html": """{% extends "base.html" %}
{% block title %}Все посты{% endblock %}
{% block content %}
{% for post in pagination.items %}
<article>
<h2>{{ post.title }}</h2>
<p>{{ post.excerpt }}</p>
</article>
{% else %}
<p>Постов пока нет.</p>
{% endfor %}
{% endblock %}""",
}
env = Environment(loader=DictLoader(templates), trim_blocks=True, lstrip_blocks=True)
# имитация объекта Pagination из Flask-SQLAlchemy
class Pagination:
def __init__(self, items):
self.items = items
pagination = Pagination([
{"title": "Зачем я завёл блог", "excerpt": "Первая запись: мотивация и план."},
{"title": "Flask за выходные", "excerpt": "Как я собрал первый сайт."},
])
print(env.get_template("post_list.html").render(pagination=pagination))
<!doctype html> <html lang="ru"> <head> <title>Все посты</title> </head> <body> <header><a href="/">Все посты</a> | <a href="/create">Написать</a></header> <article> <h2>Зачем я завёл блог</h2> <p>Первая запись: мотивация и план.</p> </article> <article> <h2>Flask за выходные</h2> <p>Как я собрал первый сайт.</p> </article> </body> </html>
Класс Pagination имитирует реальный объект: шаблон читает pagination.items, ровно как будет во Flask. Почему имитация классом, а не словарём? У словаря в Python есть собственный метод .items, и Jinja2 вернул бы его вместо ключа — при подготовке этого урока я поймал на таком словаре TypeError: 'builtin_function_or_method' object is not iterable. Атрибуты объекта работают без сюрпризов. Тег {% else %} внутри цикла — запасной путь для пустого списка: вместо него отрисуется «Постов пока нет» — то самое поведение error_out=False при /?page=999. В render_template вы отправите настоящий Pagination, и шаблон не придётся менять ни на букву.
from jinja2 import Template
from datetime import datetime
post = {
"title": "Flask за выходные",
"created": datetime(2025, 3, 14, 18, 30),
"body": "В пятницу я решил собрать сайт. К вечеру воскресенья он работал.",
}
t = Template("""<article>
<h2>{{ post.title }}</h2>
<time>{{ post.created.strftime('%d.%m.%Y') }}</time>
<p>{{ post.body[:60] }}{% if post.body | length > 60 %}...{% endif %}</p>
</article>""", trim_blocks=True, lstrip_blocks=True)
print(t.render(post=post))
<article> <h2>Flask за выходные</h2> <time>14.03.2025</time> <p>В пятницу я решил собрать сайт. К вечеру воскресенья он рабо...</p> </article>
Здесь два приёма, которые стоят того, чтобы их разглядеть. Первый: в шаблоне вызываются настоящие методы — strftime форматирует дату, срез [:60] отрезает цитату. Второй: посмотрите на вывод — текст обрезан посреди слова («рабо...»). Работает честно, но выглядит неопрятно. В настоящем проекте я делаю цитату свойством модели (post.excerpt), которое режет текст по границе слова ещё в Python, — правило то же, что и прежде: шаблон показывает, Python готовит.
Пагинация и комментарии в шаблоне
Навигация по страницам — обычные ссылки с ?page=N, собранные из атрибутов объекта pagination. Условия показывают ссылку только когда соседняя страница существует:
from jinja2 import Template
pagination = {"page": 2, "pages": 5, "has_prev": True, "prev_num": 1,
"has_next": True, "next_num": 3}
t = Template("""<nav class="pagination">
{% if pagination.has_prev %}
<a href="/?page={{ pagination.prev_num }}">Назад</a>
{% endif %}
<span>Стр. {{ pagination.page }} из {{ pagination.pages }}</span>
{% if pagination.has_next %}
<a href="/?page={{ pagination.next_num }}">Вперёд</a>
{% endif %}
</nav>""", trim_blocks=True, lstrip_blocks=True)
print(t.render(pagination=pagination))
<nav class="pagination"> <a href="/?page=1">Назад</a> <span>Стр. 2 из 5</span> <a href="/?page=3">Вперёд</a> </nav>
На первой странице блок с «Назад» исчезнет целиком — has_prev равен False. Для десятка страниц этого достаточно; когда их станут сотни, пригодится метод iter_pages(), который рисует окно номеров вроде «1 2 3 … 12 13 14». И завершающий кусок — блок комментариев, где нужны сразу цикл, счётчик и заглушка:
from jinja2 import Template
comments = [
{"author": "Марина", "text": "Спасибо, собрала блог за вечер!"},
{"author": "Кирилл", "text": "А как добавить лайки?"},
]
t = Template("""<h2>Комментарии ({{ comments | length }})</h2>
{% for c in comments %}
<div class="comment">
<b>{{ c.author }}</b> пишет:
<p>{{ c.text }}</p>
</div>
{% else %}
<p>Пока тишина. Будьте первым!</p>
{% endfor %}""", trim_blocks=True, lstrip_blocks=True)
print("--- есть комментарии ---")
print(t.render(comments=comments))
print("--- пустой список ---")
print(t.render(comments=[]))
--- есть комментарии --- <h2>Комментарии (2)</h2> <div class="comment"> <b>Марина</b> пишет: <p>Спасибо, собрала блог за вечер!</p> </div> <div class="comment"> <b>Кирилл</b> пишет: <p>А как добавить лайки?</p> </div> --- пустой список --- <h2>Комментарии (0)</h2> <p>Пока тишина. Будьте первым!</p>
В реальном post_detail.html вы передадите в шаблон объект поста и будете ходить по post.comments — тот самый relationship из models.py. Счётчик в заголовке даёт фильтр | length. А фраза «Будьте первым» — это не украшение: приглашение под пустым блоком заметно повышает долю первых комментариев, психология работает и в вебе.
Как задеплоить Flask на PythonAnywhere?
Деплой на PythonAnywhere словами
Сайт на 127.0.0.1 виден только вам. Бесплатный и самый простой путь для Flask — PythonAnywhere: облачный хостинг, где на бесплатном тарифе живёт один небольшой сайт по адресу имя.pythonanywhere.com. Никаких серверов и Docker на этом этапе не нужно. Порядок действий такой:
- Заведите аккаунт и откройте вкладку Console — вам понадобится Bash-консоль.
- Загрузите проект: git clone вашего репозитория или загрузка файлов через интерфейс Files.
- В консоли создайте виртуальное окружение: mkvirtualenv blogenv, затем pip install flask flask-sqlalchemy.
- Вкладка Web — Add new web app — Manual configuration — укажите это окружение в разделе Virtualenv.
- Откройте WSGI-файл (ссылка есть на вкладке Web) и впишите в него свой проект — см. код ниже.
- Нажмите Reload и откройте свой адрес. Сайт живой.
import sys
path = "/home/ivanov/myblog"
if path not in sys.path:
sys.path.insert(0, path)
from app import app as application
Чек-лист готовности сайта и что дальше
Прежде чем делиться ссылкой, пройдитесь по списку. Каждый пункт — то, что отличает «работает у меня» от «работает у всех»:
@app.errorhandler(404)
def page_not_found(e):
return render_template("404.html"), 404
- debug=False, SECRET_KEY из переменной окружения — он не в репозитории.
- requirements.txt зафиксирован: pip freeze > requirements.txt.
- Путь к базе абсолютный, у blog.db настроен бэкап — это единственный файл с вашими данными.
- Обработчики 404 и 500 показывают страницы в дизайне блога.
- У каждой формы есть валидация и флеш-сообщения, удаление — только по POST.
- Никаких |safe на пользовательском вводе — автоэкранирование работает.
- Вход и регистрация подключены (урок 9), посты привязаны к авторам.
- На страницах заполнены title и meta description, есть favicon.
- Сайт проверен с телефона: шапка, формы, пагинация, комментарии.
Курс собран в этот чек-лист целиком: маршруты из второго урока, шаблоны из третьего, формы из четвёртого, база из пятого, сессии из шестого, структура из седьмого. Блог из этого урока — не конец, а фундамент: добавьте теги к постам (связь многие-ко-многим, следующая ступень после один-ко-многим), ленту по авторам, а потом прикрутите авторизацию из урока девять и получите полноценный многопользовательский блог.
А если после блога захочется другой стороны Python-веба — не страниц, а API — идите в первый урок FastAPI: вы удивитесь, сколько понятий из этого курса вам уже знакомо. Курс пройден: самоучитель Flask свернул десять уроков в один работающий блог — держите его как шпаргалку, когда забудется, где ставится кука, а где session.pop. Мой практический совет напоследок: не останавливайтесь на моём коде. Сломайте блог намеренно — удалите commit, откройте /?page=abc, отправьте пустую форму — и посмотрите, как проект реагирует. Настоящее понимание начинается с починки.
Сначала предскажи ответ в голове — это главный навык программиста.
from jinja2 import Template
t = Template("{% for c in comments %}{{ c.author }};{% else %}пока тишина{% endfor %}")
print(t.render(comments=[]))
print(t.render(comments=[{"author": "Марина"}]))
from jinja2 import Template
p = {"page": 2, "pages": 3}
t = Template("{{ p.page }} из {{ p.pages }}{% if p.page < p.pages %} (есть ещё){% endif %}")
print(t.render(p=p))
1. Что делает cascade="all, delete-orphan" в relationship между Post и Comment?
2. Почему в request.args.get("page", 1, type=int) важен аргумент type=int?
3. Что вернёт db.get_or_404(Post, 999), если поста с id=999 нет?
4. Зачем после успешного POST возвращают redirect(url_for(...))?
5. Что покажет главная страница при error_out=False и адресе /?page=999, если всего 3 страницы?
Соберите шаблон списка постов для главной блога. Для каждого поста — строка вида <li>Заголовок - N комм.</li>: заголовок выведите фильтром capitalize, а количество комментариев возьмите из поля comments. Если комментариев ноль — вместо числа напишите «пока тихо». Весь список обёрнут в <ul>...</ul>. Флаги trim_blocks и lstrip_blocks уже включены, поэтому теги Jinja2 не оставят лишних пустых строк.
Нужны ли Blueprint для небольшого проекта на Flask?
Нет: блог из этого урока живёт в двух Python-файлах, и шести маршрутам отдельные модули не нужны. Блюпринты окупаются, когда маршрутов становится двадцать, имена функций конфликтуют или появился второй смысловой блок — сам механизм разбирался в уроке про Blueprint.
Как задеплоить Flask-приложение на PythonAnywhere?
Загрузите файлы проекта, создайте виртуальное окружение с flask и flask-sqlalchemy, затем во вкладке Web добавьте веб-приложение с ручной настройкой и укажите это окружение. В WSGI-файле пропишите путь к проекту в sys.path и импортируйте приложение как application: from app import app as application. После Reload сайт откроется по адресу имя.pythonanywhere.com.
Как сделать пагинацию во Flask?
У Flask-SQLAlchemy есть метод paginate: Post.query.order_by(Post.created.desc()).paginate(page=page, per_page=5, error_out=False). В шаблон передают объект pagination: pagination.items — записи текущей страницы, а has_prev, prev_num, has_next, next_num собирают ссылки «Назад» и «Вперёд» вида /?page=2.
Что делает cascade в модели SQLAlchemy?
cascade="all, delete-orphan" в relationship означает: при удалении родителя (поста) автоматически удаляются все дочерние записи (комментарии). Без этого остаются строки-сироты с битым внешним ключом post_id, которые потом всплывают багами на страницах.
Какой проект на Flask подойдёт в портфолио новичку?
Блог с постами и комментариями: он покрывает CRUD, формы, базу данных, шаблоны и деплой — и при этом понятен работодателю без объяснений. Дополните его авторизацией и выложите на PythonAnywhere: живая ссылка на работающий сайт ценится выше перечисления технологий в резюме.
Понравился урок? Сошлитесь на него
«Первый проект должен заработать целиком, а не на три четверти.»
Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.
Что читать дальше
Flask · Урок 9
Регистрация и вход: аутентификация на Flask
Девятый урок курса Flask: модель User, хеширование паролей, маршруты register/login/logout и декоратор login_required. Демо хеширования sha256 с солью запускается прямо на странице.
Flask · Урок 5
База данных в Flask: SQLite и Flask-SQLAlchemy
Пятый урок курса Flask: данные переживают перезапуск сервера. Подключаем SQLite, описываем модели Flask-SQLAlchemy, проходим CRUD и собираем гостевую книгу, которая помнит всех гостей.
FastAPI · Урок 1
Что такое API и REST: введение в FastAPI для начинающих
Понять, что такое API, REST, HTTP-методы и JSON — и подготовиться к первому приложению на FastAPI.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
FastAPI · Урок 10
Проект: REST API сервиса заметок с базой данных
Сквозной проект курса: сервис заметок с тегами, поиском, SQLite и JWT. Структура по файлам, контракты Pydantic, тесты и README — портфолио-проект за пару вечеров.
fastapi проектfastapi crud с базой данных
FastAPI · Урок 5
CRUD на FastAPI: POST, PUT, PATCH и DELETE методы
Строим полный CRUD на FastAPI: POST со статусом 201, PUT с защитой от 404, PATCH и DELETE — и собираем мини-сервис задач целиком.
fastapi crudcrud приложение python
Flask · Урок 1
Что такое Flask: знакомство с микрофреймворком
Первый урок самоучителя Flask: как устроен веб, зачем нужен WSGI, чем микрофреймворк отличается от Django — и как за семь строк получить работающий сайт.
flaskflask python
Проверьте знания по Flask
В челлендже — 20 задач по Flask, по 2 из каждого урока этого раздела. Формат: фрагмент кода и четыре варианта — что напечатает. После ответа — вердикт и объяснение со ссылкой на урок-источник.
Тест по Flask: 20 задач