Use Restly in an Existing Project#

RestView provides CRUD endpoints for a model. Register a view on the same FastAPI app or APIRouter as your existing routes. Add one resource at a time. Individual endpoints can remain plain FastAPI routes.

Add a RestView Next to Existing Routes#

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

from fastapi import APIRouter, FastAPI
import fastapi_restly as fr

from .db import async_engine          # the async engine your app already builds
from .orders import router as orders_router

app = FastAPI()
fr.configure(app, async_engine=async_engine)

api = APIRouter(prefix="/api")

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


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


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

app.include_router(api)

fr.include_view works as a direct call (fr.include_view(api, UserView), above) or as a class decorator (@fr.include_view(app), used later in this guide); both register the same routes. Pass app to fr.configure, also when the views are on a router: then Restly can check the app’s OpenAPI spec for two classes with the same name (Name your schemas).

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

You can also keep plain FastAPI routes beside a view 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 default CRUD route should be hand-written, exclude it and add the FastAPI route yourself:

class UserView(fr.AsyncRestView):
    prefix = "/users"
    model = User
    schema = UserSchema
    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 UserView 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 method (get_many, get_one, create, update, delete); for a different status code, response shape, or query interface, see Customizing RestView.

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#

To replace a hand-written CRUD router with a RestView, map the existing HTTP contract first:

  1. Map your routes to the view’s default CRUD contract. GET /, POST /, and GET/PATCH/DELETE on /{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.

A resource in mid-migration looks like this:

class ProductView(fr.AsyncRestView):
    prefix = "/products"
    model = Product
    schema = ProductSchema          # 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 most common integration is reusing the engine (or sessionmaker) your app already builds, 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)

fr.configure() builds the session factory on top. RestView commits its write actions. Your application still controls the engine. Use a session generator (next section) only when sessions must be constructed in a custom way, such as scoped sessions, multi-tenant routing, or 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 or roll back on the way out). RestView commits its write actions.

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]:
    async with MyAsyncSession() as session:
        yield session

# Before registering routes or including views.
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]:
    with MySession() as session:
        yield session

# Before registering routes or including views.
fr.configure(sync_session_generator=my_get_db)

Restly resolves the configured generator through FastAPI’s dependency graph. An existing Depends(my_get_db) in an authentication dependency or route and the view’s self.session receive the same session within a request. That means a user loaded by authentication can be assigned directly to a relationship in the view. The generator runs once, with one cleanup, under FastAPI’s normal dependency caching. Custom use_cache=False, dependency scopes, and security scopes follow FastAPI’s own caching rules.

Configure the generator before registering any route that uses fr.SessionDep or fr.AsyncSessionDep, including plain FastAPI routes. FastAPI fixes the dependency graph at registration, so setting or replacing that generator later raises RestlyConfigurationError. An explicit session: Annotated[..., Depends(reporting_get_db)] on a view keeps using reporting_get_db.

Sharing also works when Restly owns session creation. Use its public aliases in your application’s dependencies and plain routes. This example assumes your application supplies User and current_user_id():

fr.configure(async_engine=existing_async_engine)

async def get_current_user(session: fr.AsyncSessionDep):
    return await session.get(User, current_user_id())

Views and application dependencies using fr.AsyncSessionDep share one session. Use fr.SessionDep for sync code. Both aliases keep Restly’s uncommitted-changes warning, including when used on plain routes.

The test fixtures override the configured generator through app.dependency_overrides under rollback isolation. Native application dependencies and Restly then share an isolated request session. The generator body does not run for requests during those tests. If no factory is configured, setup opens the generator to discover its session’s bind. Generators needing request arguments require an explicit test engine or sessionmaker as well. For your own overrides, use app.dependency_overrides[my_get_db] = test_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. RestView owns the commit.
    # The context manager 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 = ReportSchema
    session: ReportingSessionDep

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

Use the Configured Session Off-Request#

Outside the request cycle (in background tasks, scripts, or workers), open a session from FastAPI-Restly’s configured factory directly with fr.open_async_session() or fr.open_session():

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 a RestView:

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

RestView supports these models for default 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.