A REST Framework for FastAPI
Project description
FastAPI-Restly
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]" aiosqlite
Docs: https://www.fastapi-restly.org/ · Changelog · Contributing · Security · Examples
Why FastAPI-Restly?
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 resources, 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 throughIDRef[...]. - React Admin ready:
AsyncReactAdminViewspeaksra-data-simple-rest. - App utilities: SQLAlchemy engine/session setup, exception handlers, and test fixtures.
Coming from another CRUD generator? See How Restly compares.
Quickstart
FastAPI-Restly turns a SQLAlchemy model into a class-based CRUD resource. (To make it fully runnable, add dev table creation — see Getting Started.)
import fastapi_restly as fr
from fastapi import FastAPI
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
app = FastAPI()
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]
@fr.include_view(app)
class UserView(fr.AsyncRestView):
prefix = "/users"
model = User
That 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 user
Restly generates the Pydantic schemas automatically.
Not just CRUD
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 four 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 context
The 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?
- Shared dependencies are typed attributes you read from
self(self.session,self.current_user). - 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.
Installation
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.
Main features
Manual schema definition
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 UserRead
Restly 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 = UserUpdate
Use auto-schema for prototypes and internal tools. Use an explicit schema for public contracts, aliases, and strict validation.
List endpoint query parameters
List endpoints expose a stable URL parameter dialect generated from the response schema:
GET /users/?name=John&age__gte=21
GET /users/?email__icontains=example
GET /users/?sort=-created_at&page=2&page_size=10
Parameter 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 = 200
See Filter, Sort, and Paginate Lists for the full operator surface, alias rules, and pagination guidance.
Read-only and write-only fields
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_update
Relationship handling
Validate relationships on create and update with fr.IDRef[...].
Restly passes either the foreign key (customer_id) or the related object (Customer) to SQLAlchemy, depending on the model constructor.
class Order(fr.IDBase):
customer_id: Mapped[int] = mapped_column(ForeignKey("customer.id"))
customer: Mapped[Customer] = relationship()
class OrderRead(fr.IDSchema):
customer_id: fr.IDRef[Customer]
customer: fr.ReadOnly[CustomerRead]
Custom endpoints
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)
React Admin integration
Use AsyncReactAdminView for a react-admin
backend compatible with ra-data-simple-rest:
@fr.include_view(app)
class ProductView(fr.AsyncReactAdminView):
prefix = "/products"
model = Product
schema = ProductRead
The view speaks the ra-data-simple-rest wire contract.
See React Admin Integration in the docs for CORS setup and customization.
Excluding built-in routes
@fr.include_view(app)
class UserView(fr.AsyncRestView):
prefix = "/users"
model = User
exclude_routes = (fr.ViewRoute.DELETE,)
Pagination metadata
@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, ...}
Testing
fastapi_restly.pytest_fixtures provides client and session fixtures with
savepoint-based isolation. The testing extra auto-loads them as a pytest plugin.
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 skip the assertion and inspect the response yourself.
Configuration
# 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.
Documentation
- 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
Examples
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
Contributing
Pull requests and issue discussions welcome. See CONTRIBUTING.md for setup and tests. For security issues, see SECURITY.md.
License
MIT — see LICENSE.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file fastapi_restly-0.7.0.tar.gz.
File metadata
- Download URL: fastapi_restly-0.7.0.tar.gz
- Upload date:
- Size: 78.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
edc26e0fdb623ad335dafccccdc667b5177561e6d2381d5e1f557b2f15155848
|
|
| MD5 |
d79bb7f3d410d50d0d60d0844a9a1cd2
|
|
| BLAKE2b-256 |
337908ced9105bf6075057b245bb6866b87e046d8b9e37969c75fa0094090a4a
|
Provenance
The following attestation bundles were made for fastapi_restly-0.7.0.tar.gz:
Publisher:
publish.yml on rjprins/fastapi-restly
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
fastapi_restly-0.7.0.tar.gz -
Subject digest:
edc26e0fdb623ad335dafccccdc667b5177561e6d2381d5e1f557b2f15155848 - Sigstore transparency entry: 1792940423
- Sigstore integration time:
-
Permalink:
rjprins/fastapi-restly@57ee7e47c195e3e8076b86af722fe4cba3837a8f -
Branch / Tag:
refs/tags/v0.7.0 - Owner: https://github.com/rjprins
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@57ee7e47c195e3e8076b86af722fe4cba3837a8f -
Trigger Event:
release
-
Statement type:
File details
Details for the file fastapi_restly-0.7.0-py3-none-any.whl.
File metadata
- Download URL: fastapi_restly-0.7.0-py3-none-any.whl
- Upload date:
- Size: 83.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
36748b2e68475614280d7a8dd0455b08e50049fffa489d6d400a431e4bf69445
|
|
| MD5 |
a96f92bf1b7679c6d2acbe9bd013f9bd
|
|
| BLAKE2b-256 |
4b21ded56d073a277e6087ad75285689eceb92e148ad514847ce2a31b43c89ea
|
Provenance
The following attestation bundles were made for fastapi_restly-0.7.0-py3-none-any.whl:
Publisher:
publish.yml on rjprins/fastapi-restly
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
fastapi_restly-0.7.0-py3-none-any.whl -
Subject digest:
36748b2e68475614280d7a8dd0455b08e50049fffa489d6d400a431e4bf69445 - Sigstore transparency entry: 1792940613
- Sigstore integration time:
-
Permalink:
rjprins/fastapi-restly@57ee7e47c195e3e8076b86af722fe4cba3837a8f -
Branch / Tag:
refs/tags/v0.7.0 - Owner: https://github.com/rjprins
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@57ee7e47c195e3e8076b86af722fe4cba3837a8f -
Trigger Event:
release
-
Statement type: