Start with files. Grow into packages.
Keep the application easy to navigate while it is small, and split a feature when its responsibilities grow.
A single main.py is enough for Hello World. When you add features,
Karak's recommended layout keeps them as files inside an app package:
my-backend/
├── pyproject.toml
├── uv.lock
├── app/
│ ├── __init__.py
│ ├── main.py
│ ├── resources.py
│ ├── users.py
│ └── orders.py
└── tests/
├── __init__.py
├── test_users.py
└── test_orders.py
Leave the two __init__.py files empty. They make app and tests Python
packages. A .py file such as users.py is already a module; a package lets
that module grow into several files later.
This is a recommended convention, not a directory structure enforced by Karak.
Add each file when you need it. resources.py can wait until the application
has a shared resource, and a small feature does not need separate routes,
services, and data-access files.
Give each file a clear responsibility
| File | Responsibility |
|---|---|
app/main.py |
Create the root router, include feature routers, register resources, and construct app |
app/resources.py |
Declare shared resource factories and their startup/cleanup behavior |
app/users.py |
Define the users router, its handlers, and small user-related functions or classes |
app/orders.py |
Define the orders router, its handlers, and small order-related functions or classes |
tests/test_users.py |
Check user-related behavior |
tests/test_orders.py |
Check order-related behavior |
Keep HTTP handling in route functions: read inputs, call application behavior, and return a response. Other functions and classes can take ordinary Python values, so they are useful independently of HTTP and easy to test.
Follow routers and modules for the complete users.py, orders.py,
and main.py examples. Run the application from the project root:
uv run uvicorn app.main:app --reload
When introducing resources, put the factories in app/resources.py and register
their handles in main.py. Handlers ask for initialized values with
ResourceContext[T]. The resource guide explains that lifecycle.
Feature modules should not import app.main: it assembles the application and
already imports them. Keep reusable service code independent of application
assembly. Resource factories can import the service classes they construct.
Expand one feature when it needs more room
If users.py starts mixing substantial HTTP handling and business logic, replace
that file with a users/ package:
app/
├── __init__.py
├── main.py
├── resources.py
├── users/
│ ├── __init__.py
│ ├── routes.py
│ └── service.py
└── orders.py
Move the users router and handlers into app/users/routes.py. Move substantial
business logic into app/users/service.py; create this file only when there is
logic to move. Re-export the router from app/users/__init__.py:
from app.users.routes import router
The application entry point can keep its existing import:
from app.users import router as users_router
Replace app/users.py with the package; do not leave both forms side by side.
The URLs and router.include(users_router) stay the same. The new file layout
does not introduce a new routing layer or a URL prefix.
orders.py can remain a single file until it also benefits from being split.
There is no required file count or line limit. Split when a responsibility is
hard to find, understand, or change in the current file. Add database or schema
modules only when the feature actually has those responsibilities.
Grow tests at the same pace
Start with one test file per feature. When a feature's tests become difficult
to navigate, replace test_users.py with a package grouped by the behavior tested:
tests/
├── __init__.py
├── users/
│ ├── __init__.py
│ ├── test_routes.py
│ └── test_service.py
└── test_orders.py
Keep the __init__.py files so Python's unittest discovery can descend into
these packages. Remove the old test_users.py after moving its tests; keep test
filenames beginning with test_. Run all tests from the project root:
uv run python -m unittest discover -s tests -t . -v
Tests can grow independently of application files. A single users.py may
already need several test files, while a small users package may still fit in
one test_users.py. Add a shared tests/helpers.py only when several tests
need the same setup, and import it as from tests.helpers import ....
See testing for runnable service, HTTP, and cleanup checks. Keep the test behavior the same when moving files; reorganizing should not change what the application does.