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:
uv add fastapi-startkit --extra fastapi
# or
pip install "fastapi-startkit[fastapi]"Setup
Next, add FastAPIProvider to your providers:
# 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 theFastAPIinstance and binds it into the container withuse_fastapi().boot()registers theservecommand, sendsHTTPException, request validation errors, and uncaught exceptions through the exception manager, addsRequestLifecycleMiddleware, and makesconfig/fastapi.pyavailable to publish.
Configuration
Default Configuration
The server settings live in the FastAPIConfig dataclass:
| Field | Environment variable | Default | Purpose |
|---|---|---|---|
app_url | APP_URL | http://127.0.0.1:8000 | Host and port that serve binds to |
app | — | bootstrap.application:app | Import path Uvicorn loads |
reload | APP_RELOAD | True | Restart when code changes |
ws | APP_WS | auto | Uvicorn WebSocket implementation |
reload_dirs | — | None | Directories 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:
# .env
APP_URL=http://127.0.0.1:8000
APP_RELOAD=truePublishing 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:
uv run python artisan provider:publish --provider fastapiThis writes config/fastapi.py:
# 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:
# 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:
# 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 itemThen include the router in your provider's boot() method:
# 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:
# 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:
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:
| Route | HTTP method | Controller method |
|---|---|---|
/{name} | GET | index |
/{name}/create | GET | create |
/{name} | POST | store |
/{name}/{param} | GET | show |
/{name}/{param}/edit | GET | edit |
/{name}/{param} | PUT | update |
/{name}/{param} | DELETE | destroy |
{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:
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:
# 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()# 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:
# 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"}) # classNarrowing the registered routes
When a resource doesn't need every action, use only or excepts to register just the ones you want:
router.resource("users", users_controller, only={"index", "show"})
router.resource("users", users_controller, excepts={"create", "edit"})Custom route names and URL parameters
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:
# 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:
# 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.
uv run python artisan serveserve 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.