Skip to main content

A REST Framework for FastAPI

Project description

FastAPI-Restly

CI Python License Coverage

FastAPI-Restly logo

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 View for custom resources, or AsyncRestView / RestView for 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 / WriteOnly markers, plus foreign-key validation through IDRef[...].
  • React Admin ready: AsyncReactAdminView speaks ra-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, RestView adds 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

Examples

Complete applications under example-projects/:

  • Shop — e-commerce API with products, orders, customers
  • Blog — minimal blog with a single Blog model
  • 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

fastapi_restly-0.7.0.tar.gz (78.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

fastapi_restly-0.7.0-py3-none-any.whl (83.5 kB view details)

Uploaded Python 3

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

Hashes for fastapi_restly-0.7.0.tar.gz
Algorithm Hash digest
SHA256 edc26e0fdb623ad335dafccccdc667b5177561e6d2381d5e1f557b2f15155848
MD5 d79bb7f3d410d50d0d60d0844a9a1cd2
BLAKE2b-256 337908ced9105bf6075057b245bb6866b87e046d8b9e37969c75fa0094090a4a

See more details on using hashes here.

Provenance

The following attestation bundles were made for fastapi_restly-0.7.0.tar.gz:

Publisher: publish.yml on rjprins/fastapi-restly

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for fastapi_restly-0.7.0-py3-none-any.whl
Algorithm Hash digest
SHA256 36748b2e68475614280d7a8dd0455b08e50049fffa489d6d400a431e4bf69445
MD5 a96f92bf1b7679c6d2acbe9bd013f9bd
BLAKE2b-256 4b21ded56d073a277e6087ad75285689eceb92e148ad514847ce2a31b43c89ea

See more details on using hashes here.

Provenance

The following attestation bundles were made for fastapi_restly-0.7.0-py3-none-any.whl:

Publisher: publish.yml on rjprins/fastapi-restly

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page