Build your first Tenchi app
Create a working application, call its API, follow one request through the code, and change its behavior in a plain async function.
Requirements
Tenchi requires Python 3.12 or newer. The commands in this guide use uv to create the environment and run the application.
Create the application
uvx tenchi new my_app
cd my_app
uv syncThe generated application already contains a todos feature and SQLite persistence. Start it:
uv run tenchi devCall the API
From another terminal, create and list todos:
curl -i \
-H 'content-type: application/json' \
-d '{"title":"Buy milk"}' \
http://127.0.0.1:8000/todos
curl http://127.0.0.1:8000/todosThe create operation returns status 201, a validated Todo body, and a
Location header. The list operation returns the saved todo. Restart the
server and list again to confirm that SQLite persisted it.
Open http://127.0.0.1:8000/docs to inspect and call the same API through Swagger UI.
Follow the request through four files
The create operation has four application-facing parts:
| Path | Responsibility |
|---|---|
app/features/todos/schemas.py | Defines the validated request and response data |
app/features/todos/contracts.py | Declares POST /todos and its HTTP behavior |
app/features/todos/use_cases/create_todo.py | Implements the application behavior |
app/features/todos/routes.py | Binds the contract to the use case |
The contract and route handle the boundary. The use case stays ordinary Python:
async def create_todo(request: CreateTodo, context: AppContext) -> Todo:
return await context.todos.create(title=request.title)context.todos is a TodoRepository port owned by the application. The
running server supplies a SQLite adapter; the direct use-case test supplies a
memory adapter. The use case does not need to know which one it received.
The generated project also includes server wiring, the SQLite and memory adapters, and an HTTP test. You do not need to understand or edit them to work on this operation.
Change the behavior
Trim surrounding whitespace before saving a title. In
app/features/todos/use_cases/create_todo.py, change the repository call:
async def create_todo(request: CreateTodo, context: AppContext) -> Todo:
- return await context.todos.create(title=request.title)
+ return await context.todos.create(title=request.title.strip())Update the direct test in
app/features/todos/tests/test_create_todo.py so it proves the new behavior:
- todo = await create_todo(CreateTodo(title="Buy milk"), context)
+ todo = await create_todo(CreateTodo(title=" Buy milk "), context)
assert todo.title == "Buy milk"Run that focused test:
uv run pytest app/features/todos/tests/test_create_todo.pyThe test calls the use case directly with a memory repository. It does not start an HTTP server or open SQLite.
Check the application
uv run tenchi checktenchi check runs the generated project's formatting, linting, type,
behavior, architecture, and boundary checks. The command reports every failed
step so one run gives you the complete repair list.
Your first change touched one plain function and one focused test. The contract, route, persistence wiring, OpenAPI, and HTTP behavior remained aligned without requiring changes.
Continue from the core
- How Tenchi works explains the small application model.
- Build a feature end to end adds a persisted operation across a port, two adapters, a route, tests, and the OpenAPI snapshot.
- Prepare for production covers operational decisions when the application is ready to ship.