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:
0.5.0— first public beta release.pip install "fastapi-restly[standard]"
Docs: https://rjprins.github.io/fastapi-restly/ · Changelog · Contributing · Security · Examples
Why FastAPI-Restly?
FastAPI-Restly is a small REST resource layer on top of FastAPI, SQLAlchemy, and Pydantic:
SQLAlchemy owns persistence.
Pydantic owns validation and serialization.
FastAPI owns routing and dependency injection.
Restly owns repetitive REST resource mechanics.
The base View class is the foundation. It gives related endpoints a shared
prefix, tags, dependencies, and ordinary Python inheritance without forcing a
model or CRUD shape. RestView and AsyncRestView build on that foundation for
the common list/get/create/update/delete case.
- True class-based views — group endpoints on real Python classes with inheritance and method overrides.
- REST endpoints in minutes — use
Viewfor custom resources, orAsyncRestView/RestViewfor generated CRUD. - Class-level dependencies — apply authentication, rate limits, tenant context, or other FastAPI dependencies once per view.
- Explicit override points — replace an endpoint, a business-logic handler, or an object helper without awkward hacks.
- Modern stack — SQLAlchemy 2.0, Pydantic v2, async and sync support.
- Filtering, pagination, sorting — standard HTTP query interface generated from your response schema.
- Field control —
ReadOnly/WriteOnlymarkers, plus scalar foreign-key references viaIDRef[...]. - React Admin ready —
AsyncReactAdminViewspeaks thera-data-simple-restwire contract, no custom data provider needed. - Testing utilities —
RestlyTestClientand savepoint-based isolation fixtures.
Quickstart
FastAPI-Restly turns a SQLAlchemy model into a class-based CRUD resource:
import fastapi_restly as fr
from fastapi import FastAPI
from sqlalchemy.orm import Mapped
app = FastAPI()
class User(fr.IDBase):
name: Mapped[str]
email: Mapped[str]
@fr.include_view(app)
class UserView(fr.AsyncRestView):
prefix = "/users"
model = User
That view exposes list, create, read, patch, and delete endpoints with filtering, sorting, pagination, and an auto-generated Pydantic schema. For the full copy-paste app, database setup, and run command, see Getting Started.
For endpoints that are related but not CRUD, start with View:
from typing import Annotated
from fastapi import Depends
def get_current_user():
...
@fr.include_view(app)
class AccountView(fr.View):
prefix = "/account"
tags = ["account"]
current_user: Annotated[User, Depends(get_current_user)]
@fr.get("/me")
async def me(self) -> AccountRead:
return AccountRead.from_user(self.current_user)
@fr.post("/password")
async def change_password(self, payload: PasswordChange) -> AccountRead:
...
Annotated dependencies become instance attributes, so shared request context
lives on the view class instead of being repeated on every endpoint. The same
pattern works on RestView / AsyncRestView.
Philosophy
Restly uses a layered approach. Each layer adds convenience while letting you drop down for deeper control. The less customization you need, the more you get out-of-the-box — full customization never requires awkward hacks. Restly stays close to patterns already provided by FastAPI, Pydantic, and SQLAlchemy.
Installation (development)
git clone https://github.com/rjprins/fastapi-restly.git
cd fastapi-restly
uv sync
Advanced 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
created_at: fr.ReadOnly[datetime]
@fr.include_view(app)
class UserView(fr.AsyncRestView):
prefix = "/users"
model = User
schema = UserRead
Restly derives create and update schemas from UserRead by default. 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
creation_schema = UserCreate
update_schema = UserUpdate
Use auto-schema for prototypes and internal tools. Use an explicit schema when contract stability and validation control matter (public APIs, aliases, strict response shapes).
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/?status=active,pending # comma-separated → OR (IN)
GET /users/?status__ne=archived,deleted # comma-separated → NOT IN
GET /users/?email__icontains=example
GET /users/?deleted_at__isnull=true
GET /users/?sort=-created_at,name
GET /users/?page=2&page_size=10
Parameter keys follow the response schema's public names end-to-end —
including dotted relation paths. If ArticleRead.author has
Field(alias="writer") and AuthorRead.name has
Field(alias="authorName"), the URL key is writer.authorName. Aliased
fields are only reachable by their alias; populate_by_name does not
extend the URL surface with the Python field name.
Pagination is opt-in: omitting page_size returns every matching row.
For public/production endpoints set default_page_size and
max_page_size on the view class:
class UserView(fr.AsyncRestView):
default_page_size = 25
max_page_size = 200
See How-To: 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] # cannot be set in requests
Relationships
from sqlalchemy import ForeignKey
from sqlalchemy.orm import Mapped, mapped_column
class Order(fr.IDBase):
customer_id: Mapped[int] = mapped_column(ForeignKey("customer.id"))
total: Mapped[float]
class OrderRead(fr.IDSchema):
customer_id: fr.IDRef[Customer] # wire format: 123 — resolved to FK
total: float
Custom endpoints and handlers
Add endpoints with @fr.get, @fr.post, @fr.put, @fr.patch, @fr.delete, or the generic @fr.route. Override perform_* handlers (perform_listing, perform_get, perform_create, ...) to customise built-in CRUD logic without replacing the endpoint.
@fr.include_view(app)
class UserView(fr.AsyncRestView):
prefix = "/users"
model = User
schema = UserRead
@fr.get("/{id}/download")
async def download_user(self, id: int):
return {"id": id, "status": "ok"}
async def perform_listing(self, query_params):
# Custom logic here
return await super().perform_listing(query_params)
React Admin integration
Use AsyncReactAdminView to get a backend that react-admin with ra-data-simple-rest connects to out of the box:
@fr.include_view(app)
class ProductView(fr.AsyncReactAdminView):
prefix = "/products"
model = Product
schema = ProductRead
The view speaks the ra-data-simple-rest wire contract:
- List — translates
sort=["name","ASC"],range=[0,24], andfilter={"name":"foo"}into SQL and returns a JSON array with aContent-Range: items 0-24/315header. - All other CRUD —
GET /{id},POST /,PATCH /{id},DELETE /{id}work unchanged.
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 namespaced pytest fixtures (restly_app, restly_client, restly_async_session, restly_session) for test clients and savepoint-based isolation. The testing extra installs a pytest plugin entry point, so pytest auto-loads these fixtures.
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 automatically asserts the expected HTTP status (200 for GET, 201 for POST, 204 for DELETE, ...) and raises a descriptive AssertionError with 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")
Restly has one public process-wide configuration. For per-view databases, read replicas, or other custom session wiring, use a normal FastAPI dependency on that view; see the existing-project how-to in the documentation.
Documentation
- Getting Started — fast path from zero to a working API
- 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, coding standards, and the test workflow. Security issues: see SECURITY.md.
License
MIT — see LICENSE.
Project details
Release history Release notifications | RSS feed
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.5.0.tar.gz.
File metadata
- Download URL: fastapi_restly-0.5.0.tar.gz
- Upload date:
- Size: 62.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
631cd5c01d39f6987fdeed04b5f9dd4cf2a6b3a6454062fcbb80c6207b6334e8
|
|
| MD5 |
966fc550531ebefa2154d0b320878ed3
|
|
| BLAKE2b-256 |
0353bf039849e68e1f43845c9d81e14931fd7348d3e322d248d2e23adfaee09f
|
Provenance
The following attestation bundles were made for fastapi_restly-0.5.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.5.0.tar.gz -
Subject digest:
631cd5c01d39f6987fdeed04b5f9dd4cf2a6b3a6454062fcbb80c6207b6334e8 - Sigstore transparency entry: 1452507487
- Sigstore integration time:
-
Permalink:
rjprins/fastapi-restly@2c038263be85eb31bbaed07b70b0871b20b3f3e4 -
Branch / Tag:
refs/tags/v0.5.0 - Owner: https://github.com/rjprins
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@2c038263be85eb31bbaed07b70b0871b20b3f3e4 -
Trigger Event:
release
-
Statement type:
File details
Details for the file fastapi_restly-0.5.0-py3-none-any.whl.
File metadata
- Download URL: fastapi_restly-0.5.0-py3-none-any.whl
- Upload date:
- Size: 66.8 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 |
7f058a0a41543e572cc56678f2bb860f6955c838630945b3a66132ef9f00be56
|
|
| MD5 |
f6d4ad39ad215490f16d7d93aca20a16
|
|
| BLAKE2b-256 |
54d99f5cbe461e5bc094138fc7a32b15169f2830da27cfceb536ff7fbbf5ef6c
|
Provenance
The following attestation bundles were made for fastapi_restly-0.5.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.5.0-py3-none-any.whl -
Subject digest:
7f058a0a41543e572cc56678f2bb860f6955c838630945b3a66132ef9f00be56 - Sigstore transparency entry: 1452507578
- Sigstore integration time:
-
Permalink:
rjprins/fastapi-restly@2c038263be85eb31bbaed07b70b0871b20b3f3e4 -
Branch / Tag:
refs/tags/v0.5.0 - Owner: https://github.com/rjprins
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@2c038263be85eb31bbaed07b70b0871b20b3f3e4 -
Trigger Event:
release
-
Statement type: