DEV Community

Cover image for Add a Django-style admin to your FastAPI app in ~10 minutes
Seva D
Seva D

Posted on

Add a Django-style admin to your FastAPI app in ~10 minutes

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]"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Run it:

uvicorn main:app --reload
Enter fullscreen mode Exit fullscreen mode

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:

FastAdmin dashboard demo

Going further

The same ModelAdmin style scales past CRUD when you need it:

  • Bulk actions — decorate a method with @action and 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_file hook (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)