Skip to content

Mounting routes

MSFLib modules do not register endpoints on your app. Each one exposes router factories: functions that take your session dependency, your current-account dependency, your settings and your model types, and return an APIRouter. You decide the prefix, the tags and what sits in front of them. This guide covers the options and what each one affects.

The two prefix layers

A factory has a prefix argument, and include_router has its own. They concatenate. The factory prefix is part of the router, and the include_router prefix is applied when you attach it.

from fastapi import APIRouter, FastAPI
from msflib.auth.router import router as auth_router

api = APIRouter()
api.include_router(
    auth_router(
        get_session=get_session,
        get_keystore=get_keystore,
        get_current_account=auth_deps.get_current_account,
        account_type=Account,
        account_read_type=AccountRead,
        settings=settings,
        prefix="",
    )
)

app = FastAPI()
app.include_router(api, prefix="/api/v1")

This serves POST /api/v1/login, which is the layout of the reference host app. Leave the factory default instead (prefix="/auth") and the same login is POST /api/v1/auth/login. The defaults and routes of the factories used in this guide:

Factory Default prefix Routes
msflib.auth.router.router /auth POST /login, DELETE /logout, POST /password-recovery, POST /verify-token, POST /reset-password, GET /google/login, POST /google/callback
msflib.account.router.account_router /account GET /me, PUT /me, POST /open (path set by open_registration_path or the ACCOUNT setting OPEN_REGISTRATION_PATH), POST /verify-availability
msflib.account.router.account_admin_router /admin/accounts Admin list, create, read and update of accounts
msflib.account.router.profile_router /profiles Profile read, create, update and avatar
msflib.workspaces.router.router /workspaces Workspace list, available list, create, read, update and delete

Other modules (documents, ingestion, conversation, AI) follow the same pattern; their factory arguments are on their module pages.

Keep the token URL in step

get_account_dependencies(..., oauth_token_url=...) is the URL that the interactive API docs use for the Authorize button. Set it to the real login path, for example "/api/v1/login". It does not affect how tokens are validated.

Use your own dependencies

Every dependency a router needs is an argument, so you can wrap the shared ones. Here, the account endpoints require an account that your app has marked as verified. The wrapped dependency is passed in place of get_current_account, and the prefix is changed at the same time.

from fastapi import Depends, HTTPException


def get_verified_account(account=Depends(auth_deps.get_current_account)):
    if not (account.data or {}).get("verified"):
        raise HTTPException(status_code=403, detail="Verify your email first")
    return account


accounts = account_router(
    get_session=get_session,
    get_current_account=get_verified_account,
    settings=settings,
    account_type=Account,
    account_read_type=AccountRead,
    profile_type=Profile,
    account_action=aa,
    profile_action=pa,
    prefix="/people",
)
app.include_router(accounts, prefix="/api/v2")

GET /api/v2/people/me now answers 403 for an unverified account. Only the routes that depend on get_current_account are affected: POST /api/v2/people/open stays public, because registration needs no identity.

The same idea applies to the other callables. role_check for the admin and workspace routers takes a class such as auth_deps.RoleCheck (the router calls it with its roles= list). get_current_workspace, get_current_tenant and similar arguments on the other routers take dependency callables in the same way.

Put a router behind extra checks

include_router(..., dependencies=[...]) adds dependencies to every route in the router:

from fastapi import Depends

app.include_router(
    accounts,
    prefix="/api/v2",
    dependencies=[Depends(auth_deps.get_current_active_account)],
)

Router-wide dependencies also cover public routes

This applies to every route, including the ones meant for anonymous callers. With the example above, POST /open returns 401 to a caller who has no token yet, and the same dependency on the auth router would block POST /login. Use router-wide dependencies on routers that are entirely protected, and wrap get_current_account (previous section) when only some routes should change.

The reference host app uses this form to validate a conversation id before any AI agent route runs (testsite/app/api/api.py).

Shadow a route

When two routes have the same method and path, FastAPI uses the first one registered. To replace a module endpoint, declare yours before you include the module's router:

from fastapi import APIRouter, Depends

accounts = account_router(
    get_session=get_session,
    get_current_account=auth_deps.get_current_account,
    settings=settings,
    account_type=Account,
    account_read_type=AccountRead,
    profile_type=Profile,
    account_action=aa,
    profile_action=pa,
    prefix="",
)

v2 = APIRouter(prefix="/v2")


@v2.get("/accounts/me")
def my_me(account=Depends(auth_deps.get_current_account)):
    return {"custom": True, "email": account.email}


v2.include_router(accounts, prefix="/accounts")
app.include_router(v2)

The module's factory prefix is empty here, so its route is /me before include_router adds /accounts; a route with a different factory prefix (such as /people) would end up at a different path and nothing would be shadowed. GET /v2/accounts/me returns your response, and the module's other account routes are still served. The module's version of the route stays in the router and in the OpenAPI document, so use a distinct path if the duplicate in the docs is a problem.

Remove routes you do not want

A router has no option to leave routes out. The routes are a plain list, so filter it before you include the router. Paths in the list include the factory prefix.

accounts = account_router(..., prefix="/people")
accounts.routes[:] = [r for r in accounts.routes if r.path != "/people/verify-availability"]
app.include_router(accounts, prefix="/api/v2")

POST /api/v2/people/verify-availability is then a 404. This depends on the router's internals, not on a documented option, so keep a test that checks the route list, and prefer wrapping a dependency when what you want is to restrict a route rather than remove it.

For routes you must not expose at all, such as google/login and google/callback when you do not use Google sign-in, note that they already answer 403 unless the AUTH setting ENABLE_GOOGLE_OAUTH is true.

Mount the same module twice

Factories return fresh routers, so two calls with different arguments produce independent routers: for example a public registration surface and an admin surface for accounts, or /api/v1 and /api/v2 with different dependencies. Reuse the same actions and dependencies, and vary the prefix and the callables.

Workspace-scoped paths

If routes should live under a workspace, put the path parameter in the prefix and use dependencies built with the matching resolver (see Authentication). The repository's workspace tests mount the AI provider router like this:

from msflib.ai_core.router import provider_router as create_ai_core_router

ws_user_deps = get_user_dependencies(...)  # as in Authentication, "Workspaces"

ai_core_router = create_ai_core_router(
    get_session=get_session,
    settings=settings,
    get_current_workspace=ws_user_deps.get_current_workspace,
    get_current_workspace_anonymous=ws_user_deps.get_current_workspace_anonymous,
    get_current_active_user=ws_user_deps.get_current_active_user,
    workspace_role_check=ws_user_deps.WorkspaceRoleCheck,
    prefix="/{workspace_slug}/ai/providers",
)

Testing mounted routes

Override the session and keystore with app.dependency_overrides, using the same function objects you passed to the factories. See the end of Writing custom code.