If you've moved to FastAPI from Django, there's one thing you probably miss on
day one: django-admin. You define a model, register it, and you get a real
CRUD back-office for free — list views, filters, search, edit forms, the works.
FastAPI is deliberately unopinionated, so it ships nothing like that. For a lot
of internal tools, that back-office is exactly what you need and don't want to
hand-build.
This walkthrough adds a Django-admin-style dashboard to a FastAPI + SQLAlchemy
app in about ten minutes, using FastAdmin.
Full disclosure up front: I maintain FastAdmin. I'll point out where other
tools fit better at the end so this isn't a one-sided pitch.
What we're building
A /admin dashboard for a tiny blog: User and Post models, superuser
sign-in, list views with filters and search, and edit forms — without writing a
line of frontend code. The React UI ships bundled in the package, so there's no
Node.js step at install time.
1. Install
pip install "fastadmin[fastapi,sqlalchemy]"
The extras pull in the FastAPI glue and the async SQLAlchemy backend. (Swap in
tortoise, django, or pony if that's your ORM — FastAdmin has a first-class
admin class for each.)
2. Define your models
Nothing FastAdmin-specific here — these are plain async SQLAlchemy models.
# models.py
from sqlalchemy import Boolean, ForeignKey, Integer, String, Text
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
engine = create_async_engine("sqlite+aiosqlite:///db.sqlite3")
session_maker = async_sessionmaker(engine, expire_on_commit=False)
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "user"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
username: Mapped[str] = mapped_column(String(255), nullable=False)
hash_password: Mapped[str] = mapped_column(String(255), nullable=False)
is_superuser: Mapped[bool] = mapped_column(Boolean, default=False)
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
def __str__(self) -> str:
return self.username
class Post(Base):
__tablename__ = "post"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
title: Mapped[str] = mapped_column(String(255), nullable=False)
body: Mapped[str] = mapped_column(Text, default="")
is_published: Mapped[bool] = mapped_column(Boolean, default=False)
author_id: Mapped[int] = mapped_column(ForeignKey("user.id"))
author: Mapped["User"] = relationship()
def __str__(self) -> str:
return self.title
3. Register your admins
This is the part that looks like Django. You subclass a base admin
(SqlAlchemyModelAdmin here) and describe how each model shows up. The one
required piece is an authenticate method on the user model's admin — it's how
sign-in works, against your user table, however you hash passwords.
# admin.py
import os
# Must be set before importing fastadmin
os.environ["ADMIN_USER_MODEL"] = "User"
os.environ["ADMIN_USER_MODEL_USERNAME_FIELD"] = "username"
os.environ["ADMIN_SECRET_KEY"] = "change-me-in-production"
import bcrypt
from sqlalchemy import select
from fastadmin import SqlAlchemyModelAdmin, register
from models import Post, User, session_maker
@register(User, sqlalchemy_sessionmaker=session_maker)
class UserAdmin(SqlAlchemyModelAdmin):
exclude = ("hash_password",)
list_display = ("id", "username", "is_superuser", "is_active")
list_display_links = ("id", "username")
list_filter = ("is_superuser", "is_active")
search_fields = ("username",)
async def authenticate(self, username: str, password: str) -> int | None:
sessionmaker = self.get_sessionmaker()
async with sessionmaker() as session:
result = await session.scalars(
select(User).filter_by(username=username, is_superuser=True)
)
user = result.first()
if not user or not bcrypt.checkpw(password.encode(), user.hash_password.encode()):
return None
return user.id
@register(Post, sqlalchemy_sessionmaker=session_maker)
class PostAdmin(SqlAlchemyModelAdmin):
list_display = ("id", "title", "author", "is_published")
list_display_links = ("id", "title")
list_filter = ("is_published",)
search_fields = ("title", "body")
If you've written a django-admin ModelAdmin, list_display / list_filter
/ search_fields need no explanation. That's the whole point.
4. Mount it on FastAPI
The admin is just an ASGI sub-app you mount wherever you like:
# main.py
from contextlib import asynccontextmanager
import bcrypt
from fastapi import FastAPI
from fastadmin import fastapi_app as admin_app
from models import Base, User, engine, session_maker
import admin # noqa: F401 — importing registers the admin classes
@asynccontextmanager
async def lifespan(app: FastAPI):
# Create tables and a superuser to sign in with (demo only)
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
async with session_maker() as session:
session.add(
User(
username="admin",
hash_password=bcrypt.hashpw(b"admin", bcrypt.gensalt()).decode(),
is_superuser=True,
)
)
await session.commit()
yield
app = FastAPI(lifespan=lifespan)
app.mount("/admin", admin_app)
Run it:
uvicorn main:app --reload
Open http://localhost:8000/admin, sign in with admin / admin, and you've
got a working dashboard over your models.
What you actually get
Out of that ~60 lines: paginated list views, column sorting, the filters and
search you declared, add/edit forms with the right widget per field type, and
delete — all in a polished antd React UI. Here's the demo from the repo:
Going further
The same ModelAdmin style scales past CRUD when you need it:
-
Bulk actions — decorate a method with
@actionand it shows up in the actions dropdown on the list page (e.g. "publish selected posts"). -
Inlines — edit related rows (a tournament's events, a post's comments) on
the parent's page via
inlines. -
Form widgets — 20+ antd widgets (rich text, JSON editor, async select,
slug, date/time pickers) via
formfield_overrides. -
File & image uploads — a storage-agnostic
upload_filehook (local, S3, presigned URLs). - Dashboard charts — declarative line/area/column/bar/pie widgets for a metrics view.
When not to reach for this
Being honest about fit:
- You're on Django. Just use the built-in admin — it's right there and it's excellent. FastAdmin's story is strongest off Django (FastAPI/Flask).
- You're SQLAlchemy-only and want the smallest possible dependency. sqladmin is SQLAlchemy-focused and great at that. starlette-admin is another strong, flexible option (SQLAlchemy, MongoEngine, ODMantic).
Where FastAdmin earns its place is the combination: multiple frameworks
(FastAPI, Flask, Django) and multiple ORMs (SQLAlchemy, Tortoise, Django, Pony,
Yara) behind one Django-admin-style API, with the React UI bundled so there's
no separate frontend build.
If that matches your stack, the repo and docs are here:
Happy to answer questions in the comments — and if you hit a rough edge, issues
and PRs are welcome.

Top comments (0)