Your first API

Build a small API with a module, a controller and an injected provider.

You will build a two-endpoint API and see dependency injection resolve a provider without registering it anywhere.

You need Python 3.12+ and Bustan 0.0.2.

Install#

pip install bustan==0.0.2

A provider#

# app/tasks/service.py
from bustan import Injectable

@Injectable()
class TaskService:
    def __init__(self) -> None:
        self._tasks: dict[int, str] = {}
        self._next_id = 1

    def add(self, title: str) -> int:
        task_id = self._next_id
        self._tasks[task_id] = title
        self._next_id += 1
        return task_id

    def all(self) -> dict[int, str]:
        return dict(self._tasks)

A controller#

Since 0.0.2, request bodies are declared as Pydantic models rather than loose Body() parameters:

# app/tasks/controller.py
from pydantic import BaseModel
from bustan import Controller, Get, Post
from .service import TaskService

class CreateTask(BaseModel):
    title: str

@Controller("/tasks")
class TaskController:
    def __init__(self, tasks: TaskService) -> None:
        self.tasks = tasks

    @Get("/")
    async def list_tasks(self) -> dict[int, str]:
        return self.tasks.all()

    @Post("/")
    async def create(self, payload: CreateTask) -> dict[str, int]:
        return {"id": self.tasks.add(payload.title)}

A malformed body now fails validation before your handler runs, and the error shape is consistent across the app.

A module#

# app/tasks/module.py
from bustan import Module
from .controller import TaskController
from .service import TaskService

@Module(controllers=[TaskController], providers=[TaskService])
class TaskModule:
    pass
# app/main.py
from bustan import Module, Bustan
from .tasks.module import TaskModule

@Module(imports=[TaskModule])
class AppModule:
    pass

app = Bustan(AppModule)

Run it#

uvicorn app.main:app --reload
$ curl -s -X POST localhost:8000/tasks/ -d '{"title":"write docs"}' -H 'content-type: application/json'
{"id":1}
$ curl -s -X POST localhost:8000/tasks/ -d '{}' -H 'content-type: application/json'
{"error":"validation_failed","detail":[{"field":"title","message":"field required"}]}

Upgrading from 0.0.1#

0.0.1 0.0.2
title: str = Body() a Pydantic model parameter
guards, interceptors and pipes
bustan test utilities
Bustan(AppModule) unchanged

Body() still works and warns. It is removed in 0.1.0.

Next#

  • Write tests for this — see the next tutorial
  • Add a guard to protect a route — see the how-to guides