Skip to content

Routers & Structure Interview Questions & Answers

13 questions Updated 2026-06-20 Share:

FastAPI APIRouter interview questions — include_router, prefix, tags, dependencies, nested routers, versioning and large app structure.

13 of 13

APIRouter is a mini-app that collects route definitions and is then mounted onto the main FastAPI instance. It lets you split routes across files without a circular import on the app object.

# routers/items.py
from fastapi import APIRouter
router = APIRouter()

@router.get("/")
async def list_items(): ...

@router.post("/")
async def create_item(): ...
# main.py
from fastapi import FastAPI
from routers import items

app = FastAPI()
app.include_router(items.router, prefix="/items", tags=["items"])

Rule of thumb: one router per resource/domain; import and mount them all in main.py — this is the FastAPI equivalent of Flask Blueprints.

prefix prepends a path segment to every route in the router. tags groups every route under a documentation category in Swagger UI.

app.include_router(
    users_router,
    prefix="/users",      # /list → /users/list
    tags=["users"],        # grouped under "users" in docs
)
app.include_router(
    orders_router,
    prefix="/orders",
    tags=["orders"],
)

You can also set prefix and tags on the APIRouter constructor — useful when the router "owns" its prefix. Setting them in include_router is cleaner for versioned mounts.

Rule of thumb: set prefix and tags on APIRouter() if the router always belongs to one resource; set them in include_router if you mount the same router at multiple prefixes (e.g., versioning).

Pass dependencies=[Depends(fn)] to the APIRouter constructor or to include_router:

from fastapi import APIRouter, Depends
from .auth import verify_api_key

router = APIRouter(
    prefix="/admin",
    tags=["admin"],
    dependencies=[Depends(verify_api_key)],
)

@router.get("/stats")
async def stats(): ...  # verify_api_key runs automatically

Dependencies at the router level run before route-level dependencies. Both sets stack — they don't replace each other.

Rule of thumb: put auth/rate-limit dependencies on the router so new routes can't accidentally bypass them.

Call parent_router.include_router(child_router, prefix=...):

# routers/orders/items.py
items_router = APIRouter()

@items_router.get("/")
async def list_order_items(): ...

# routers/orders/__init__.py
orders_router = APIRouter(prefix="/orders")
orders_router.include_router(items_router, prefix="/{order_id}/items")

# main.py
app.include_router(orders_router)
# → GET /orders/{order_id}/items/

Nesting is unlimited; prefixes concatenate.

Rule of thumb: nest routers to mirror resource hierarchy in the URL — it keeps path prefixes DRY and makes the app structure easy to navigate.

Not directly on APIRouter, but you can set responses (error response docs) and dependencies at the router level. Default response_model and status_code must still be set per-route.

A common pattern is to use route_class to inject default behaviour:

from fastapi.routing import APIRoute

class LoggedRoute(APIRoute):
    def get_route_handler(self):
        original = super().get_route_handler()
        async def custom(request):
            log_request(request)
            return await original(request)
        return custom

router = APIRouter(route_class=LoggedRoute)

Rule of thumb: use route_class for cross-cutting concerns (logging, timing); set response_model and status_code per route for explicitness.

Pass responses to include_router. These are merged with per-route responses in the OpenAPI schema.

app.include_router(
    admin_router,
    prefix="/admin",
    responses={
        401: {"description": "Not authenticated"},
        403: {"description": "Forbidden"},
    },
)

All routes in admin_router now document 401 and 403 in the schema without repeating it on every decorator.

Rule of thumb: declare common error responses (401, 403, 429) at the router level; declare business-logic errors (404, 409) at the individual route level.

include_router integrates routes into the same FastAPI app — they share middleware, exception handlers, dependency injection and the OpenAPI schema.

mount attaches a separate ASGI application at a path prefix. The mounted app has its own middleware and schema; requests to its prefix are fully delegated.

# include_router — routes join the parent app
app.include_router(users_router, prefix="/users")

# mount — separate sub-app, own /docs
v2_app = FastAPI()
app.mount("/v2", v2_app)

mount is appropriate for: serving static files (StaticFiles), mounting a separate versioned API that has diverged significantly, or embedding non-FastAPI ASGI apps.

Rule of thumb: use include_router for 95% of cases; use mount when you genuinely need a separate ASGI application with independent middleware.

app/
├── main.py            # FastAPI() instance, include all routers
├── dependencies.py    # shared Depends() functions
├── models/
│   ├── user.py        # Pydantic + ORM models
│   └── order.py
├── routers/
│   ├── users.py       # APIRouter for /users
│   └── orders.py      # APIRouter for /orders
├── services/
│   ├── user_service.py
│   └── order_service.py
└── db/
    ├── session.py     # engine + get_db dependency
    └── models.py      # SQLAlchemy ORM models
# main.py
from fastapi import FastAPI
from app.routers import users, orders

app = FastAPI()
app.include_router(users.router, prefix="/users", tags=["users"])
app.include_router(orders.router, prefix="/orders", tags=["orders"])

Rule of thumb: keep route handlers thin — they call service functions, not business logic directly; separate ORM models from Pydantic schemas.

No — APIRouter has no built-in lifespan support. Lifespan events belong to the FastAPI app. For modular startup/shutdown, compose context managers inside the single app lifespan:

from contextlib import asynccontextmanager

@asynccontextmanager
async def db_lifespan(app):
    await db.connect()
    yield
    await db.disconnect()

@asynccontextmanager
async def cache_lifespan(app):
    await cache.connect()
    yield
    await cache.disconnect()

@asynccontextmanager
async def app_lifespan(app):
    async with db_lifespan(app):
        async with cache_lifespan(app):
            yield

app = FastAPI(lifespan=app_lifespan)

Rule of thumb: compose multiple async context managers inside the app's lifespan function rather than trying to distribute startup across routers.

Pass default_response_class to the APIRouter:

from fastapi import APIRouter
from fastapi.responses import ORJSONResponse

router = APIRouter(default_response_class=ORJSONResponse)

@router.get("/items")
async def list_items():
    return [{"id": 1}]   # serialised via orjson automatically

Or set it on the FastAPI() instance to affect all routes:

app = FastAPI(default_response_class=ORJSONResponse)

Rule of thumb: set ORJSONResponse globally on the app for consistent fast serialisation; override per-route only for special cases like HTMLResponse.

Wrap the router in a minimal FastAPI app in the test file:

from fastapi import FastAPI
from fastapi.testclient import TestClient
from app.routers.users import router

test_app = FastAPI()
test_app.include_router(router, prefix="/users")

client = TestClient(test_app)

def test_list_users():
    resp = client.get("/users/")
    assert resp.status_code == 200

Override dependencies on test_app with test_app.dependency_overrides to inject fakes without touching the real app object.

Rule of thumb: always test routers through a minimal FastAPI wrapper — it exercises the full request/response pipeline including middleware and DI.

Use a simple if guard before include_router:

import os
from fastapi import FastAPI
from app.routers import debug_tools

app = FastAPI()

if os.getenv("ENV") == "development":
    app.include_router(debug_tools.router, prefix="/debug")

This is evaluated at import time, so the routes are never registered in production even if someone guesses the URL.

Rule of thumb: gate debug/admin routers behind an env check at startup rather than include_in_schema=False — environment-gated routes truly don't exist in production, not just hidden from docs.

Two approaches:

Option A — prefix every include_router call:

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

Option B — mount the FastAPI app on a prefix using Starlette's root_path:

app = FastAPI(root_path="/api/v1")

root_path adjusts the OpenAPI servers base URL but doesn't actually prefix routes in the ASGI routing — use a reverse proxy rewrite for that.

The cleanest approach is to create a single top-level router:

api_router = APIRouter(prefix="/api/v1")
api_router.include_router(users_router, prefix="/users")
app.include_router(api_router)

Rule of thumb: collect all routers into one api_router with the version prefix, then mount it once — adding a new version means creating a second top-level router.

More ways to practice

The self-quiz is live. Get notified when mock interviews and new question packs drop.

or
Join our WhatsApp Channel