Use Restly in an Existing Project#

FastAPI-Restly registers ordinary FastAPI path operations. You can mount Restly views on the same FastAPI app or APIRouter as your existing routes, adopt it one resource at a time, and move individual endpoints back to plain FastAPI when that is the clearer shape.

Add Restly Next to Existing Routes#

Use fr.include_view(...) wherever you already compose routes. Existing routers and Restly views can share the same parent app or router:

from fastapi import APIRouter, FastAPI
import fastapi_restly as fr

from .orders import router as orders_router

app = FastAPI()
api = APIRouter(prefix="/api")

api.include_router(orders_router, prefix="/orders")  # existing FastAPI routes


class UserView(fr.AsyncRestView):
    prefix = "/users"
    model = User
    schema = UserRead


fr.include_view(api, UserView)  # generated /api/users routes

app.include_router(api)

Adoption is per resource. In the example above, orders stay hand-written while users use Restly. Adding a ProductView later does not require changing the orders router.

You can also keep plain FastAPI routes beside a Restly resource for endpoints that are not part of the CRUD surface:

@api.get("/users/{id}/export")
async def export_user(id: int):
    ...

Step Out for One Endpoint#

If one generated endpoint should be hand-written, exclude only that route and add the FastAPI route yourself:

class UserView(fr.AsyncRestView):
    prefix = "/users"
    model = User
    schema = UserRead
    exclude_routes = (fr.ViewRoute.DELETE,)


fr.include_view(api, UserView)


@api.delete("/users/{id}", status_code=204)
async def delete_user(id: int):
    ...

That leaves Restly responsible for list, create, read, and update while DELETE uses your ordinary FastAPI implementation. For smaller changes that keep the same HTTP contract, prefer overriding the business verb (get_many, get_one, create, update, delete) or its handle_<verb> request handler; for a different status code, response shape, or query interface, see Override CRUD Behavior and Add Custom Endpoints.

Step Out for a Whole Resource#

There is no global Restly router to unwind. A resource is included only where you call fr.include_view(...). To move a resource back to plain FastAPI, remove that include call and register an APIRouter with the same prefix and path operations.

Your models, schemas, dependencies, and session wiring can stay in ordinary app modules.

Replace an Existing Hand-Written Router#

Going the other direction — retiring a hand-written CRUD router in favor of a generated view — is a mapping exercise:

  1. Map your routes to the generated table. GET / + POST / + GET/PATCH/DELETE /{id} are covered (the exact contract). Anything else on the router (exports, actions) stays as custom routes on the view or as plain FastAPI routes beside it.

  2. Keep custom semantics out of the swap. A route whose contract differs (e.g. PUT updates, a non-204 delete) can be excluded via exclude_routes and kept hand-written until you adapt it.

  3. Pin the wire contract with tests first. Write RestlyTestClient tests against the old router’s responses, then swap in the view and run them unchanged — payload or status drift shows up immediately.

class ProductView(fr.AsyncRestView):
    prefix = "/products"
    model = Product
    schema = ProductRead          # match your old response shape exactly
    exclude_routes = (fr.ViewRoute.DELETE,)  # old DELETE returns the object


fr.include_view(api, ProductView)
api.include_router(legacy_delete_router)  # until the contract is adapted

Reuse Your Existing Engine#

The common integration: your app already builds an engine (or sessionmaker) with the pool settings and URL handling you trust. Hand exactly that object to fr.configure() — Restly does not need to own it:

import fastapi_restly as fr

# the engine your app already creates somewhere central
fr.configure(async_engine=existing_async_engine)

# sync apps: fr.configure(engine=existing_engine)
# or hand over a sessionmaker instead:
#   fr.configure(async_make_session=ExistingAsyncSession)

Restly builds its session factory on top and owns the commit on its views; nothing about the engine changes hands. Reach for a session generator (next section) only when sessions must be constructed in a custom way — scoped sessions, multi-tenant routing, instrumentation.

Provide Your Own Session Generator#

If your project already manages its own database sessions, configure FastAPI-Restly to use them instead of its built-in session factory.

If you provide custom sessionmakers or generators, make sure their lifecycle and session options match the behavior your views rely on. Restly’s built-in factories intentionally use different autoflush defaults for sync and async sessions and keep expire_on_commit=False for both; see Session Factory Defaults. A custom generator constructs sessions your way but does not own the commit: it should construct, yield, and clean up (close / roll back on the way out); Restly commits. Customizing how a session is built never takes the commit away from Restly.

For async views (AsyncRestView), pass an async generator to fr.configure():

from typing import AsyncIterator
from sqlalchemy.ext.asyncio import AsyncSession
import fastapi_restly as fr

async def my_get_db() -> AsyncIterator[AsyncSession]:
    ...
    yield MyAsyncSession()

fr.configure(session_generator=my_get_db)

For sync views (RestView), pass a sync generator:

from typing import Iterator
from sqlalchemy.orm import Session
import fastapi_restly as fr

def my_get_db() -> Iterator[Session]:
    ...
    yield MySession()

fr.configure(sync_session_generator=my_get_db)

Use a Custom Session Dependency on One View#

Use fr.configure(...) when one session source should be the default for the application. If only one view should use a different session source, override the view’s session dependency instead.

from collections.abc import AsyncIterator
from typing import Annotated

from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine

import fastapi_restly as fr


reporting_engine = create_async_engine("postgresql+asyncpg://user:pass@reports/db")
ReportingSession = async_sessionmaker(
    bind=reporting_engine,
    autoflush=False,
    expire_on_commit=False,
)


async def get_reporting_db() -> AsyncIterator[AsyncSession]:
    # Construct, yield, and clean up -- do NOT commit. Restly owns the commit;
    # the ``async with`` rolls back and closes the session on the way out.
    async with ReportingSession() as session:
        yield session


ReportingSessionDep = Annotated[AsyncSession, Depends(get_reporting_db)]


@fr.include_view(app)
class ReportView(fr.AsyncRestView):
    prefix = "/reports"
    model = Report
    schema = ReportRead
    session: ReportingSessionDep

The custom dependency owns session construction and cleanup. Restly still owns the commit. Use this for read replicas, reporting databases, or other per-view session wiring.

You can also use FastAPI-Restly’s configured session proxy directly in your own code (for example in background tasks):

import fastapi_restly as fr

async with fr.open_async_session() as session:
    result = await session.execute(...)

# sync counterpart:
with fr.open_session() as session:
    result = session.execute(...)

Off-request code owns its commit — these helpers do not commit for you.

Use Your Own DeclarativeBase Models#

If your project already has SQLAlchemy models on a custom DeclarativeBase, you can use those models directly in FastAPI-Restly views:

import fastapi_restly as fr
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column


class AppBase(DeclarativeBase):
    pass


class World(AppBase):
    __tablename__ = "world"
    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
    message: Mapped[str]


@fr.include_view(app)
class WorldView(fr.AsyncRestView):
    prefix = "/world"
    model = World

FastAPI-Restly supports these models for generated CRUD routes and auto-generated schemas. When creating tables, use your own base metadata (for example AppBase.metadata.create_all(...)).

See also#

  • Test APIs with RestlyTestClient and Fixtures — pin the wire contract while migrating; savepoint-isolated tests against your DB.

  • Deploying — engine configuration from environment values, Alembic, and a production main.py.

  • Patterns — the idiomatic answers for nested resources, webhooks, and other shapes your existing app probably has.