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:
Map your routes to the view’s default CRUD contract.
GET /,POST /, andGET/PATCH/DELETEon/{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.Keep custom semantics out of the swap. A route whose contract differs (e.g.
PUTupdates, a non-204 delete) can be excluded viaexclude_routesand kept hand-written until you adapt it.Pin the wire contract with tests first. Write
RestlyTestClienttests 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.