Try FastAPI-Restly in your browser: the badge above opens a GitHub Codespace with the dev environment installed, ready to run a live example API.
Build maintainable REST APIs on FastAPI, SQLAlchemy 2.0, and Pydantic v2 — with real class-based views.
Status: public beta release (changelog).
Restly is public after four years of internal use. The API is settling on the way to
1.0.0; expect small breaking changes in deeper extension points. Feedback is welcome.
pip install "fastapi-restly[standard]" aiosqliteDocs: https://www.fastapi-restly.org/ · Changelog · Contributing · Security · Examples
Restly turns SQLAlchemy models into FastAPI resources without hiding FastAPI. Its class-based views are real Python classes: use inheritance, mixins, and method overrides to share behavior across resources.
- Class-based views: group endpoints on Python classes with inheritance and method overrides.
- REST endpoints in minutes: use
Viewfor custom endpoint groups, orAsyncRestView/RestViewfor generated CRUD. - Incremental adoption: use Restly per resource; drop to ordinary FastAPI when needed — see Existing Project Integration.
- Class-level dependencies: declare shared dependencies once and read their values from
self. - Explicit override points: change the route shell, request handler, or business verb.
- Filtering, pagination, sorting: get schema-derived list parameters.
- Field control:
ReadOnly/WriteOnlymarkers, plus foreign-key validation throughMustExist[...]. - React Admin ready:
AsyncReactAdminView/ReactAdminViewspeakra-data-simple-rest. - App utilities: SQLAlchemy engine/session setup, exception handlers, and test fixtures.
FastAPI-Restly turns a SQLAlchemy model into a class-based CRUD resource. This example is complete and runnable; it creates SQLite tables at startup for local development. Use Alembic migrations in production.
from contextlib import asynccontextmanager
import fastapi_restly as fr
from fastapi import FastAPI
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
fr.configure(async_database_url="sqlite+aiosqlite:///app.db")
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "user"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str]
email: Mapped[str]
@asynccontextmanager
async def lifespan(_app: FastAPI):
await fr.db.async_create_all(Base) # dev tables; use Alembic in production
yield
app = FastAPI(lifespan=lifespan)
@fr.include_view(app)
class UserView(fr.AsyncRestView):
prefix = "/users"
model = UserThat view exposes these HTTP routes:
GET /users/ # list users, with filtering, sorting, and pagination
POST /users/ # create a user
GET /users/{id} # read one user
PATCH /users/{id} # partially update one user
DELETE /users/{id} # delete one userRestly generates the Pydantic schemas automatically.
The Quickstart uses plain SQLAlchemy on purpose — Restly doesn't hide it. In
real projects you'll usually inherit fr.IDBase for models and fr.IDSchema
for schemas, which supply the id for you; both appear in the examples below.
View is the same class-based machinery without the generated routes — use it
to group related non-CRUD endpoints (auth flows, webhook receivers, actions):
@fr.include_view(app)
class AuthView(fr.View):
prefix = "/auth"
tags = ["auth"]
session: fr.AsyncSessionDep
@fr.post("/login")
async def login(self, credentials: LoginRequest) -> Token: ...
@fr.post("/logout")
async def logout(self) -> None: ...And because views truly subclass, the biggest everyday win takes only a few lines:
declare your app's request context once on a base view, and read it from
self everywhere — instead of re-declaring the same Depends parameters on
every function in the project:
class AppView(fr.View):
session: fr.AsyncSessionDep
current_user: Annotated[User, Depends(get_current_user)]
class ProfileView(AppView): ... # custom endpoint groups
class AppRestView(AppView, fr.AsyncRestView): ... # CRUD resources, same contextThe rule of thumb: a plain FastAPI route for a one-off endpoint, View for a
group of related custom endpoints, AsyncRestView / RestView for a CRUD
resource, and RestView plus custom @fr.post methods for CRUD with actions.
Why a View over a bare APIRouter?
- Prefix, tags, responses, and dependencies are declared once on the class.
- Views compose: inheritance and mixins share behavior across endpoint groups.
- One registration call (
fr.include_view) per class, bindable to any app or router. - And when a group grows into a resource,
RestViewadds generated CRUD and lifecycle hooks on the same class shape.
The install command at the top is the whole story: the standard extra brings
the fastapi dev toolchain, and aiosqlite is the async SQLite driver used in
the examples (Restly is driver-agnostic — use asyncpg/psycopg for
PostgreSQL). Details, including the [testing] extra, are in
Getting Started.
For custom validation, aliases, or stable public contracts, define an explicit read schema:
from datetime import datetime
class UserRead(fr.IDSchema):
name: str
email: str
password: fr.WriteOnly[str]
created_at: fr.ReadOnly[datetime]
@fr.include_view(app)
class UserView(fr.AsyncRestView):
prefix = "/users"
model = User
schema = UserRead
# schema_create = UserCreate # auto-generated from UserRead
# schema_update = UserUpdate # auto-generated from UserReadRestly derives create and update schemas from UserRead by default.
The UserCreate schema is created by omitting ReadOnly fields.
The UserUpdate schema allows for partial updates by making all fields optional.
When you need full control over write payloads, declare them explicitly:
class UserCreate(fr.BaseSchema):
name: str
email: str
class UserUpdate(fr.BaseSchema):
name: str | None = None
email: str | None = None
@fr.include_view(app)
class UserView(fr.AsyncRestView):
prefix = "/users"
model = User
schema = UserRead
schema_create = UserCreate
schema_update = UserUpdateUse auto-schema for prototypes and internal tools. Use an explicit schema for public contracts, aliases, and strict validation.
List endpoints expose a stable URL parameter dialect generated from the response schema:
GET /users/?name=John&created_at__gte=2024-01-01
GET /users/?email__icontains=example
GET /users/?sort=-created_at&page=2&page_size=10Parameter keys use the response schema's public names, including dotted
relation paths; unknown keys are rejected with 422.
Pagination is opt-in: omitting page_size returns every matching row. For
public endpoints, set default_page_size and max_page_size on the view:
class UserView(fr.AsyncRestView):
default_page_size = 25
max_page_size = 200See Filter, Sort, and Paginate Lists for the full operator surface, alias rules, and pagination guidance.
IDSchema already provides a read-only id, so don't redeclare it unless you need to narrow the type.
class UserRead(fr.IDSchema):
name: str
email: str
password: fr.WriteOnly[str] # stripped by to_response_schema()
created_at: fr.ReadOnly[datetime] # excluded from schema_create / schema_updateValidate a foreign-key column on create and update with fr.MustExist[int, Model].
It keeps the plain id (customer_id) and checks the referenced row exists; declare the relationship (customer) separately when you also want the nested object.
class Order(fr.IDBase):
customer_id: Mapped[int] = mapped_column(ForeignKey("customer.id"))
customer: Mapped[Customer] = relationship()
class OrderRead(fr.IDSchema):
customer_id: fr.MustExist[int, Customer]
customer: fr.ReadOnly[CustomerRead]Add custom routes with FastAPI-style decorators.
@fr.get@fr.post@fr.put@fr.patch@fr.delete@fr.route
They forward keyword arguments to FastAPI's route registration.
class UploadView(fr.AsyncRestView):
prefix = "/uploads"
model = Upload
@fr.get(
"/{id}/download",
response_class=FileResponse,
responses={200: {"content": {EXCEL_MIME_TYPE: {}}}},
)
async def download_excel(self, id: int):
upload = await self.handle_get_one(id)
return to_excel_response(upload)Use AsyncReactAdminView (or ReactAdminView for a sync stack) for a
react-admin backend compatible with
ra-data-simple-rest:
@fr.include_view(app)
class ProductView(fr.AsyncReactAdminView):
prefix = "/products"
model = Product
schema = ProductReadThe view speaks the ra-data-simple-rest wire contract.
See React Admin Integration in the docs for CORS setup and customization.
@fr.include_view(app)
class UserView(fr.AsyncRestView):
prefix = "/users"
model = User
exclude_routes = (fr.ViewRoute.DELETE,)@fr.include_view(app)
class UserView(fr.AsyncRestView):
prefix = "/users"
model = User
include_pagination_metadata = True
# Response: {"items": [...], "total": N, "page": 1, "page_size": 100, "total_pages": N, ...}fastapi_restly.pytest_fixtures provides client and session fixtures with
savepoint-based isolation. Restly registers them as a pytest plugin
automatically whenever it and pytest are installed.
Install the testing extra when consuming FastAPI-Restly as a package:
pip install "fastapi-restly[testing]"Configure Restly for your test database in conftest.py.
RestlyTestClient asserts the expected status (200 for GET, 201 for POST,
204 for DELETE, ...) and includes the response body on failure:
# test_users.py
def test_create_and_fetch_user(restly_client):
# Raises AssertionError if status != 201
response = restly_client.post("/users/", json={"name": "John", "email": "john@example.com"})
user_id = response.json()["id"]
# Raises AssertionError if status != 200
data = restly_client.get(f"/users/{user_id}").json()
assert data["name"] == "John"Pass assert_status_code=None to relax the check to any success status (2xx/3xx).
To inspect an error response, assert its exact code instead (e.g. assert_status_code=422).
# Async SQLite
fr.configure(async_database_url="sqlite+aiosqlite:///app.db")
# Async PostgreSQL
fr.configure(async_database_url="postgresql+asyncpg://user:pass@localhost/db")
# Sync SQLite
fr.configure(database_url="sqlite:///app.db")
# Or hand Restly the engine you already have — it does not need to own it
fr.configure(async_engine=existing_engine)Restly has one public process-wide configuration. For per-view databases, read replicas, or custom sessions, use a normal FastAPI dependency on that view. One rule to know up front: Restly owns the commit on its views — custom session generators construct and clean up, but never commit. For wiring Restly into an existing app's engine, sessions, and models, see Existing Project Integration.
- Getting Started — fast path from zero to a working API
- Class-Based Views — what "real class-based views" means, and when to use
ViewvsRestView - The Handle Design — the three tiers behind every CRUD verb, and which one to override
- User Guide — tutorial walkthroughs and topic guides
- API Reference — complete API docs
Complete applications under example-projects/:
- Shop — e-commerce API with products, orders, customers
- Blog — minimal blog with a single
Blogmodel - SaaS — multi-tenant project management API
Pull requests and issue discussions welcome. See CONTRIBUTING.md for setup and tests. For security issues, see SECURITY.md.
MIT — see LICENSE.
