Машинный перевод под контролем человека
Этот контент переведён с помощью машинной генерации, направляемой составленными людьми глоссариями и руководствами по стилю. Поскольку текст не проверяется вручную построчно, возможны отдельные ошибки или неестественные формулировки.
В случае любых расхождений авторитетным источником считается оригинальная версия на английском языке.
Добавляем админ-панель в FastAPI за 5 минут с помощью starlette-admin
2026-07-13
API готово. Теперь кому-то в команде нужно редактировать данные за ним: исправить опечатку в записи, снять публикацию с поста или проверить, что именно отправил пользователь. Стандартные варианты обычно обходятся дорого:
| Вариант | Недостаток |
|---|---|
| Собственный CRUD-frontend | Требует недель разработки и последующего сопровождения. |
| Прямой доступ к базе данных | Создаёт серьёзные риски для безопасности и целостности данных. |
| Django Admin / Flask Admin | Вынуждает переписывать приложение на другой framework или опирается на синхронный WSGI, который блокирует ваше асинхронное ASGI-приложение. |
| starlette-admin | Подключается к вашему приложению мгновенно и без единой строки frontend-кода. |
starlette-admin работает с любым приложением на базе Starlette — а именно таким и является FastAPI.
Это руководство проведёт вас от пустого файла до работающей back office-панели за пять минут. Вы создадите списки с пагинацией, поиск, сортируемые колонки, формы создания и редактирования, проверяемые вашими существующими Pydantic-схемами, подтверждение удаления и экспорт в CSV — всё генерируется напрямую из SQLAlchemy-модели.
Полный исполняемый код доступен в examples/11-sqla-pydantic-fastapi.
Минута 1: Установка
Понадобятся три пакета: сам admin-framework, ORM и FastAPI.
Pydantic поставляется вместе с FastAPI, что станет важным позже: админ-панель сможет переиспользовать ровно те же схемы, которые ваш API применяет для валидации.
Минуты 2 и 3: Готовое приложение
Создайте файл main.py. Это всё приложение целиком:
from contextlib import asynccontextmanager
from datetime import datetime
from fastapi import FastAPI
from sqlalchemy import String, Text, create_engine
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from starlette_admin.contrib.sqla import Admin, ModelView
engine = create_engine(
"sqlite:///blog.db", connect_args={"check_same_thread": False}
)
class Base(DeclarativeBase):
pass
class Post(Base):
__tablename__ = "posts"
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str | None] = mapped_column(String(120))
slug: Mapped[str | None] = mapped_column(String(160))
content: Mapped[str] = mapped_column(Text)
published_at: Mapped[datetime | None]
@asynccontextmanager
async def lifespan(app: FastAPI):
Base.metadata.create_all(engine)
yield
app = FastAPI(lifespan=lifespan)
admin = Admin(engine, title="Blog Admin", secret_key="dev-only-change-me")
admin.add_view(ModelView(Post, icon="fa fa-blog"))
admin.mount_to(app)
Обратите внимание на то, чего здесь нет. Ни шаблонов, ни обработчиков маршрутов для страниц панели, ни сериализаторов, ни конфигурации полей. starlette-admin считывает метаданные колонок SQLAlchemy и автоматически выводит весь интерфейс: текстовые поля ограниченной длины для двух колонок типа String, textarea для содержимого Text и виджет выбора даты и времени для published_at.
Три выделенные строки — единственные точки интеграции. Конструктор Admin привязывает engine базы данных, метод add_view регистрирует модель в боковом меню, а mount_to подключает всё это к вашему существующему FastAPI-приложению по пути /admin. Ваши API-маршруты остаются нетронутыми: админ-панель работает как смонтированное sub-application.
Минута 4: Запуск
Откройте http://127.0.0.1:8000/admin и нажмите Post в боковом меню. Из коробки вы получаете:
- Список всех постов с пагинацией и сортировкой.
- Формы создания и редактирования с корректным widget для каждого типа колонки.
- Страницу детального просмотра каждой записи.
- Пакетное удаление с диалогом подтверждения.
- Экспорт текущего списка в CSV и Excel.
Ваш API продолжает обслуживать трафик в штатном режиме. Проверьте http://127.0.0.1:8000/docs, чтобы убедиться, что всё работает как прежде.
Минута 5: Делаем панель по-настоящему своей
Вид по умолчанию предоставляет полноценный CRUD-интерфейс, но настоящая back office-панель заслуживает индивидуальной настройки: вашего порядка полей, вашей компоновки форм и вашего поведения поиска. Именно при наследовании от ModelView раскрывается весь потенциал starlette-admin. Замените вызов add_view на настроенное представление:
from starlette_admin import ComputedField, SlugField
class PostView(ModelView):
fields = [
"id",
"title",
SlugField("slug", populate_from="title"),
ComputedField(
"word_count",
label="Word Count",
getter=lambda request, post: len((post.content or "").split()),
),
"content",
"published_at",
]
form_layout = [("title", "slug"), "content", "published_at"]
exclude_fields_from_create = ("word_count",)
exclude_fields_from_edit = ("word_count",)
searchable_fields = ("title", "slug", "content", "published_at")
fields_default_sort = (("published_at", True),)
search_auto_submit = True
admin.add_view(PostView(Post, icon="fa fa-blog", menu_label="Blog Posts"))
В одном этом классе происходят четыре мощных улучшения:
SlugField(populate_from="title"): slug генерируется автоматически по мере ввода заголовка оператором — без единой строки собственного JavaScript.ComputedField: отображает значение, которого нет в базе данных. Количество слов вычисляется обычной Python-функцией в момент отрисовки.form_layout: раскладывает форму по логическим строкам: заголовок и slug рядом, содержимое на всю ширину, дата публикации ниже.search_auto_submit: фильтрует список динамически по мере ввода во всех колонках, перечисленных вsearchable_fields.
Отсекаем некорректные данные: используйте уже готовую схему
Операторы ошибаются, а значит, админ-панель обязана применять ваши правила на стороне сервера. Преимущество в том, что эти правила вы уже написали. Каждый проект на FastAPI валидирует тела запросов моделями Pydantic, поэтому где-то в вашей кодовой базе наверняка есть схема вроде такой:
from pydantic import BaseModel, Field, field_validator
class PostIn(BaseModel):
id: int | None = None
title: str = Field(min_length=3, max_length=120)
slug: str = Field(
min_length=3, max_length=160, pattern=r"^[a-z0-9]+(?:-[a-z0-9]+)*$"
)
content: str = Field(min_length=10)
published_at: datetime | None = None
@field_validator("content")
@classmethod
def validate_word_count(cls, v: str) -> str:
if len(v.split()) < 3:
raise ValueError("Must contain at least 3 words")
return v
Вместо того чтобы писать логику валидации дважды, передайте админ-панели вашу существующую модель. Расширение ext.pydantic предоставляет класс ModelView, который пропускает каждую отправленную форму через Pydantic-модель до того, как она попадёт в базу данных. Направьте импорт ModelView на расширение, оставьте Admin как есть и укажите схему:
from starlette_admin.contrib.sqla.ext.pydantic import ModelView
class PostView(ModelView):
... # configuration from Minute 5, unchanged
admin.add_view(
PostView(Post, pydantic_model=PostIn, icon="fa fa-blog", menu_label="Blog Posts")
)
Тело класса PostView остаётся ровно тем же; меняется лишь его базовый класс благодаря новому импорту.
Интеграция бесшовна. При создании и редактировании срабатывают все ограничения: границы длины, регулярное выражение для slug и собственный field_validator. Каждая ошибка Pydantic отображается непосредственно у соответствующего поля формы прямо внутри неё, неотличимо от формы, написанной вручную. Не забудьте оставить поле id необязательным в схеме, чтобы формы создания, у которых изначально нет ID, тоже проходили валидацию.
Так устанавливается единый источник истины. Когда в схему вашего API добавляется новое правило, админ-панель начинает применять его уже со следующего запроса — без каких-либо изменений в её собственном коде.
Есть свободная минута? Даём постам автора
Реальные данные строятся на связях, и админ-панель обрабатывает их с тем же подходом «ноль конфигурации». Добавьте модель User и свяжите её с Post:
from sqlalchemy import ForeignKey
from sqlalchemy.orm import relationship
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
full_name: Mapped[str | None] = mapped_column(String(100))
email: Mapped[str | None] = mapped_column(String(255))
website: Mapped[str | None] = mapped_column(String(512))
posts: Mapped[list["Post"]] = relationship(back_populates="user")
class Post(Base):
# ... columns from before ...
user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"))
user: Mapped["User"] = relationship(back_populates="posts")
Зарегистрируйте модель пользователя по той же схеме, основанной на схемах Pydantic. Валидация форматов EmailStr и HttpUrl выполняется автоматически, а пакет email-validator уже входит в состав fastapi[standard]:
from pydantic import EmailStr, HttpUrl
class UserIn(BaseModel):
id: int | None = None
full_name: str = Field(min_length=3)
email: EmailStr
website: HttpUrl
admin.add_view(ModelView(User, pydantic_model=UserIn, icon="fa fa-users"))
Поскольку на этот раз настраивать нечего, расширение ModelView используется напрямую, без наследования.
Наконец, сделайте автора обязательным, добавив две строки в PostIn:
class PostIn(BaseModel):
# validation runs after relations are resolved, so user is an ORM instance
model_config = ConfigDict(arbitrary_types_allowed=True)
# ... fields from before ...
user: User
У поля user: User нет значения по умолчанию, поэтому пост без автора будет отклонён так же, как любая другая ошибка валидации. Типом выступает сам класс User из SQLAlchemy, потому что админ-панель преобразует выбранный ID в ORM-экземпляр ещё до запуска валидации. Именно поэтому требуется параметр arbitrary_types_allowed (ConfigDict импортируется из pydantic).
Далее добавьте "user" в PostView.fields и form_layout, чтобы автор появился в форме поста. Это поле — не обычный выпадающий список. Это select с автодополнением на стороне сервера, который ищет ваших пользователей по мере ввода, а страница пользователя содержит обратные ссылки на все связанные посты.
Note
Функция create_all не изменяет существующие таблицы, поэтому перед перезапуском придётся удалить файл blog.db, чтобы появилась новая колонка user_id.
Перед развёртыванием
Warning
Параметр secret_key подписывает session cookie, используемый для защиты от CSRF и flash-сообщений. Замените значение-заглушку длинным случайным значением из настроек перед развёртыванием и обязательно загружайте его из переменных окружения, а не прописывайте в исходном коде.
Note
Вызов Base.metadata.create_all(engine) в lifespan — это удобство для быстрого старта. В производственном проекте таблицами управляют миграции (например, Alembic). Уберите этот вызов и направьте Admin напрямую на ваш существующий engine. starlette-admin никогда не изменяет вашу схему — он только читает и записывает строки.
Это масштабируется далеко за пределы демо
Всё описанное выше использует две модели, но те же механики ModelView способны поддерживать огромную back office-панель. Легко реализуются загрузка файлов и изображений, аутентификация с ролевым доступом, пользовательские фильтры, действия над строками и пакетные действия и полноценная i18n. Везде, где встроенного поведения недостаточно, каждый запрос и каждый этап жизненного цикла предоставляет hook для переопределения. Именно так строятся такие паттерны, как мягкое удаление с корзиной.
Что дальше
- Концепции: терминология всего, что вы только что создали, благодаря которой остальная документация будет читаться легко.
- Представления: подробный разбор всех возможностей
ModelView, включая permission hooks. - Мягкое удаление и корзина для FastAPI: первый продвинутый рецепт, построенный непосредственно на override hooks, представленных здесь.