Skip to content

FastAPI Integration ​

FastAPI Startkit doesn't replace FastAPI; it gives your FastAPI app a home inside a larger application. The FastAPIProvider takes care of the wiring for you: it creates your FastAPI application, registers the serve command, and routes exceptions through the framework's exception handler. You keep writing FastAPI the way you already know, and gain configuration, a CLI, and Laravel-style routing on top.

Installation ​

FastAPI support ships as the optional fastapi extra, so start by installing it:

bash
uv add fastapi-startkit --extra fastapi
# or
pip install "fastapi-startkit[fastapi]"

Setup ​

Next, add FastAPIProvider to your providers:

python
# bootstrap/application.py
from pathlib import Path

from fastapi_startkit import Application
from fastapi_startkit.fastapi import FastAPIProvider
from fastapi_startkit.logging import LogProvider
from config.fastapi import FastAPIConfig

app: Application = Application(
    base_path=Path(__file__).resolve().parent.parent,
    providers=[
        LogProvider,
        (FastAPIProvider, FastAPIConfig),
    ]
)

The FastAPI Provider ​

You don't need to write your own provider. Once it's registered, FastAPIProvider handles both boot phases for you:

  • register() loads the FastAPI configuration, merging the config class you pass over the framework defaults, then creates the FastAPI instance and binds it into the container with use_fastapi().
  • boot() registers the serve command, sends HTTPException, request validation errors, and uncaught exceptions through the exception manager, adds RequestLifecycleMiddleware, and makes config/fastapi.py available to publish.

Configuration ​

Default Configuration ​

The server settings live in the FastAPIConfig dataclass:

FieldEnvironment variableDefaultPurpose
app_urlAPP_URLhttp://127.0.0.1:8000Host and port that serve binds to
app—bootstrap.application:appImport path Uvicorn loads
reloadAPP_RELOADTrueRestart when code changes
wsAPP_WSautoUvicorn WebSocket implementation
reload_dirs—NoneDirectories to watch; None uses Uvicorn's default
reload_excludes—["*.log", "tests/*", "node_modules/*"]Glob patterns to ignore when watching

In practice, most applications only set APP_URL and APP_RELOAD:

bash
# .env
APP_URL=http://127.0.0.1:8000
APP_RELOAD=true

Publishing the Config ​

If you need to change a field that has no environment variable, such as reload_dirs or reload_excludes, publish the config file:

bash
uv run python artisan provider:publish --provider fastapi

This writes config/fastapi.py:

python
# config/fastapi.py
import dataclasses

from fastapi_startkit.configuration import Config
from fastapi_startkit.environment import env


@dataclasses.dataclass
class FastAPIConfig(Config):
    app_url: str = env("APP_URL", "http://127.0.0.1:8000")
    app: str = "bootstrap.application:app"
    reload: bool = env("APP_RELOAD", True)
    ws: str = env("APP_WS", "auto")
    reload_dirs: list | None = None
    reload_excludes: list = dataclasses.field(
        default_factory=lambda: [
            "*.log",
            "tests/*",
            "node_modules/*",
        ]
    )

Then pass the class with the provider, as shown in Setup. The provider merges it over the framework defaults in register(), key by key, so your class only needs the fields you actually change.

Advanced: Customizing the Provider ​

Sometimes you'll want to control the FastAPI instance yourself, for example to set the title and version or add middleware. In that case, write your own provider and register it instead of FastAPIProvider:

python
# app/providers/fastapi_provider.py
from fastapi import FastAPI
from fastapi_startkit.support import Provider
from fastapi_startkit.fastapi.commands import ServeCommand

class MyFastAPIProvider(Provider):
    def register(self) -> None:
        fastapi = FastAPI(
            title="My Custom API",
            version="1.0.0",
        )

        self.app.use_fastapi(fastapi)

    def boot(self):
        self.commands([
            ServeCommand
        ])

Keep in mind that a custom provider replaces the default one entirely, so you're responsible for registering ServeCommand and any exception handlers you still need.

Routing ​

When it comes to routing, you have a choice. You can use FastAPI's own APIRouter, or the framework's Router, which registers routes with method calls instead of decorators.

Standard FastAPI Routing ​

APIRouter works exactly as described in the FastAPI docs:

python
# routes/api.py
from fastapi import APIRouter

router = APIRouter()

@router.get("/")
async def index():
    return {"message": "Hello, FastAPI!"}

@router.post("/items")
async def create_item(item: ItemSchema):
    return item

Then include the router in your provider's boot() method:

python
# app/providers/fastapi_provider.py
class MyFastAPIProvider(Provider):
    # ... register method ...

    def boot(self):
        self.commands([ServeCommand])

        from routes.api import router
        self.app.include_router(router)

Startkit Router ​

If you've used Laravel, the Startkit Router will feel familiar. Instead of decorating each endpoint, you pass the path and the endpoint as arguments, so all of your routes can sit together in one file.

Defining Routes ​

Import Router from fastapi_startkit.fastapi and call the helper for each HTTP method with a path and an endpoint:

python
# routes/web.py
from fastapi_startkit.fastapi import Router

router = Router()

router.get("/", dashboard_controller.index)
router.post("/login", auth_controller.store)
router.put("/users/{user_id}", users_controller.update)
router.patch("/users/{user_id}", users_controller.patch)
router.delete("/logout", auth_controller.destroy)
router.head("/health", health_controller.check)
router.options("/cors", cors_controller.preflight)

The helpers also accept the keyword arguments of FastAPI's add_api_route:

python
router.get(
    "/users",
    users_controller.index,
    tags=["users"],
    summary="List all users",
    response_model=list[UserSchema],
)

Resource Routes ​

Most resources need the same set of CRUD routes. Rather than declaring each one by hand, router.resource() registers them all for a controller:

RouteHTTP methodController method
/{name}GETindex
/{name}/createGETcreate
/{name}POSTstore
/{name}/{param}GETshow
/{name}/{param}/editGETedit
/{name}/{param}PUTupdate
/{name}/{param}DELETEdestroy

{param} is the resource name without its trailing s, so users gets /users/{user}. You can rename it with parameters; the examples below call it user_id:

python
from app.http.controllers import users_controller

router.resource("users", users_controller, parameters={"users": "user_id"})

Your controller can be either a module of functions or a class:

python
# app/http/controllers/users_controller.py
async def index(request: Request):
    return await User.all()

async def show(user_id: int):
    return await User.find(user_id)

async def store(data: UserSchema):
    return await User.create(**data.model_dump())

async def update(user_id: int, data: UserSchema):
    user = await User.find(user_id)
    return await user.update(**data.model_dump())

async def destroy(user_id: int):
    user = await User.find(user_id)
    await user.delete()
python
# app/http/controllers/users_controller.py
class UsersController:
    async def index(self, request: Request):
        return await User.all()

    async def show(self, user_id: int):
        return await User.find(user_id)

    async def store(self, data: UserSchema):
        return await User.create(**data.model_dump())

    async def update(self, user_id: int, data: UserSchema):
        user = await User.find(user_id)
        return await user.update(**data.model_dump())

    async def destroy(self, user_id: int):
        user = await User.find(user_id)
        await user.delete()

You can pass either one to resource(). If you pass a class, it is instantiated for you:

python
# routes/web.py
from app.http.controllers import users_controller          # module
from app.http.controllers.users_controller import UsersController  # class

router.resource("users", users_controller, parameters={"users": "user_id"})   # module
router.resource("users", UsersController, parameters={"users": "user_id"})    # class

Narrowing the registered routes

When a resource doesn't need every action, use only or excepts to register just the ones you want:

python
router.resource("users", users_controller, only={"index", "show"})

router.resource("users", users_controller, excepts={"create", "edit"})

Custom route names and URL parameters

python
router.resource(
    "users",
    users_controller,
    names={"index": "user.list", "show": "user.detail"},
    parameters={"users": "user_id"},   # default would be "user"
)

Grouping Routes ​

To share options such as an auth dependency across several routes, put those routes on their own Router:

python
# routes/web.py
from fastapi import Depends
from fastapi_startkit.fastapi import Router
from app.http.middlewares.auth import auth

guest = Router()
guest.get("/login", auth_controller.create)
guest.post("/login", auth_controller.store)
guest.delete("/logout", auth_controller.destroy)

protected = Router(dependencies=[Depends(auth)])
protected.get("/", dashboard_controller.index)
protected.resource("users", users_controller)
protected.resource("organizations", organizations_controller)
protected.resource("contacts", contacts_controller)
protected.get("/reports", reports_controller.index)

Router(...) takes the same arguments as FastAPI's APIRouter, such as prefix and dependencies.

Registering Routers in Your Provider ​

Finally, include each router in your provider's boot() method:

python
# app/providers/fastapi_provider.py
class MyFastAPIProvider(Provider):
    # ... register method ...

    def boot(self):
        self.commands([ServeCommand])

        from routes.web import guest, protected
        self.app.include_router(guest)
        self.app.include_router(protected)

Serving the Application ​

Start the server with serve. If you're using a custom provider, remember to register ServeCommand yourself.

bash
uv run python artisan serve

serve runs Uvicorn with the app_url, reload, reload_dirs, and reload_excludes values from your FastAPI configuration.

Example Application ​

For a complete project, take a look at the FastAPI example app.