Project Structure & Conventions πΒΆ
Tatami uses smart conventions to organize your code, making projects easy to understand and maintain. This guide explains the project structure and how Tatamiβs convention-over-configuration approach works.
Why Project Structure MattersΒΆ
Good project structure:
π§ Guides developers - New team members know where to find things
π Improves maintainability - Related code stays together
β‘ Enables automation - Tatami can auto-discover components
π§ͺ Simplifies testing - Clear separation makes testing easier
π Scales well - Structure remains clean as projects grow
The Tatami Project StructureΒΆ
When you run tatami create myproject, you get this structure:
myproject/
βββ config.yaml # π§ Main configuration
βββ config-dev.yaml # π οΈ Development configuration
βββ README.md # π Project documentation
βββ favicon.ico # π¨ API favicon
βββ routers/ # π― HTTP endpoints and routing
β βββ __init__.py
βββ services/ # π§ Business logic layer
β βββ __init__.py
βββ middleware/ # π Request/response processing
β βββ __init__.py
βββ static/ # π Static files (CSS, JS, images)
βββ templates/ # π HTML templates (Jinja2)
βββ mounts/ # ποΈ Sub-applications
Letβs explore each directory in detail.
Configuration FilesΒΆ
config.yamlΒΆ
The main configuration file. Tatami uses YAML for clean, readable config:
# config.yaml
app:
title: "My Amazing API"
description: "A Tatami-powered API"
version: "1.0.0"
server:
host: "0.0.0.0"
port: 8000
database:
url: "sqlite:///./app.db"
features:
enable_docs: true
enable_cors: false
config-dev.yamlΒΆ
Development-specific overrides:
# config-dev.yaml
server:
host: "localhost"
port: 8080
database:
url: "sqlite:///./dev.db"
features:
enable_cors: true
debug: true
Use development config with:
tatami run . --mode dev
The routers/ Directory π―ΒΆ
This is where your API endpoints live. Each file becomes a router:
routers/
βββ __init__.py # Makes it a Python package
βββ users.py # User management endpoints
βββ posts.py # Blog post endpoints
βββ auth.py # Authentication endpoints
βββ admin/ # Admin endpoints (nested)
βββ __init__.py
βββ analytics.py
βββ settings.py
Router File ExampleΒΆ
# routers/users.py
from tatami import router, get, post, put, delete
from pydantic import BaseModel
from services.user_service import UserService
class User(BaseModel):
name: str
email: str
class Users(router('/users')):
"""User management endpoints"""
def __init__(self, user_service: UserService):
super().__init__()
self.user_service = user_service
@get
def list_users(self):
"""Get all users"""
return self.user_service.get_all()
@get('/{user_id}')
def get_user(self, user_id: int):
"""Get user by ID"""
return self.user_service.get_by_id(user_id)
@post
def create_user(self, user: User):
"""Create a new user"""
return self.user_service.create(user)
Router Naming ConventionsΒΆ
File names become route prefixes: users.py β /users
Class names should match the file: Users class in users.py
Method names are descriptive and use HTTP decorators
The services/ Directory π§ ΒΆ
Services contain your business logic, separated from HTTP concerns:
services/
βββ __init__.py
βββ user_service.py # User business logic
βββ email_service.py # Email sending logic
βββ payment_service.py # Payment processing
βββ data/ # Data access layer
βββ __init__.py
βββ user_repository.py
βββ post_repository.py
Service ExampleΒΆ
# services/user_service.py
from typing import List, Optional
from tatami.di import injectable
from services.data.user_repository import UserRepository
from routers.models import User, UserCreate
@injectable
class UserService:
"""Business logic for user management"""
def __init__(self, user_repo: UserRepository):
self.user_repo = user_repo
def create_user(self, user_data: UserCreate) -> User:
# Business logic: validation, rules, etc.
if self.user_repo.get_by_email(user_data.email):
raise ValueError("Email already exists")
# Create user
return self.user_repo.create(user_data)
def get_user_by_id(self, user_id: int) -> Optional[User]:
return self.user_repo.get_by_id(user_id)
def get_all_users(self) -> List[User]:
return self.user_repo.get_all()
Auto-Discovery of ServicesΒΆ
Tatami automatically discovers and makes services available for dependency injection:
# This service is automatically available for injection
from tatami.di import injectable
@injectable
class EmailService:
def send_welcome_email(self, user_email: str):
# Email sending logic
pass
# Use it in a router
class Users(router('/users')):
def __init__(self, email_service: EmailService):
self.email_service = email_service
The middleware/ Directory πΒΆ
Middleware processes requests and responses:
middleware/
βββ __init__.py
βββ auth_middleware.py # Authentication
βββ cors_middleware.py # CORS handling
βββ logging_middleware.py # Request logging
βββ rate_limit_middleware.py # Rate limiting
Middleware ExampleΒΆ
# middleware/auth_middleware.py
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.responses import Response
class AuthMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
# Check authentication
auth_header = request.headers.get('Authorization')
if not auth_header and request.url.path.startswith('/api/'):
return Response("Unauthorized", status_code=401)
# Process request
response = await call_next(request)
return response
The static/ Directory πΒΆ
Static files are automatically served at /static:
static/
βββ css/
β βββ styles.css
β βββ admin.css
βββ js/
β βββ app.js
β βββ utils.js
βββ images/
β βββ logo.png
β βββ favicon.ico
βββ docs/
βββ api_guide.pdf
Files are accessible at: - /static/css/styles.css - /static/js/app.js - /static/images/logo.png
The templates/ Directory πΒΆ
HTML templates for server-side rendering:
templates/
βββ base.html # Base template
βββ index.html # Homepage
βββ users/
β βββ list.html # User list page
β βββ detail.html # User detail page
βββ admin/
β βββ dashboard.html
β βββ reports.html
βββ __tatami__/ # π Tatami system templates
βββ docs_landing.html # Custom docs landing page
βββ swagger.html # Custom Swagger UI
βββ redoc.html # Custom ReDoc UI
Template ExampleΒΆ
<!-- templates/users/list.html -->
<!DOCTYPE html>
<html>
<head>
<title>Users - {{ app_name }}</title>
<link rel="stylesheet" href="/static/css/styles.css">
</head>
<body>
<h1>Users</h1>
<ul>
{% for user in users %}
<li>{{ user.name }} ({{ user.email }})</li>
{% endfor %}
</ul>
</body>
Customizing Tatamiβs Auto-Generated PagesΒΆ
Tatami automatically provides several pages like documentation and API explorers. You can customize these by creating templates in the special __tatami__/ directory:
Automatic Docs Landing Page
Tatami serves a landing page at /docs/ that links to all available documentation. Customize it with:
<!-- templates/__tatami__/docs_landing.html -->
<!DOCTYPE html>
<html>
<head>
<title>{{ app_name }} API Documentation</title>
<link rel="stylesheet" href="/static/css/docs.css">
</head>
<body>
<div class="docs-container">
<h1>{{ app_name }} API Documentation</h1>
<p>Welcome to the {{ app_name }} API documentation portal.</p>
<div class="docs-links">
<a href="/docs/swagger" class="docs-link">
<h3>π Swagger UI</h3>
<p>Interactive API explorer with request/response examples</p>
</a>
<a href="/docs/redoc" class="docs-link">
<h3>π ReDoc</h3>
<p>Beautiful API documentation with detailed schemas</p>
</a>
<a href="/docs/openapi.json" class="docs-link">
<h3>π OpenAPI Spec</h3>
<p>Raw OpenAPI 3.0 specification in JSON format</p>
</a>
</div>
</div>
</body>
</html>
Custom Swagger/ReDoc Templates
Override the default Swagger or ReDoc interfaces:
<!-- templates/__tatami__/swagger.html -->
<!DOCTYPE html>
<html>
<head>
<title>{{ app_name }} - Swagger UI</title>
<!-- Your custom styling -->
<link rel="stylesheet" href="/static/css/custom-swagger.css">
</head>
<body>
<!-- Custom header -->
<header class="api-header">
<h1>{{ app_name }} API Explorer</h1>
</header>
<!-- Swagger UI will be injected here -->
<div id="swagger-ui"></div>
<!-- Custom footer -->
<footer>Β© 2025 {{ app_name }}</footer>
</body>
</html>
Available Template VariablesΒΆ
In __tatami__/ templates, you have access to:
app_name
- Your application nameapp_version
- Application versionopenapi_spec
- The OpenAPI specification objectbase_url
- Base URL of your APIdocs_url
- URL to documentation landing page</html>
The mounts/ Directory ποΈΒΆ
Mount sub-applications or external ASGI apps:
mounts/
βββ admin_app.py # Admin interface
βββ docs_app.py # Custom docs app
βββ legacy_app.py # Legacy application
Mount ExampleΒΆ
# mounts/admin_app.py
from starlette.applications import Starlette
from starlette.routing import Route
from starlette.responses import JSONResponse
async def admin_dashboard(request):
return JSONResponse({"page": "admin dashboard"})
# This gets mounted at /admin
admin_app = Starlette(routes=[
Route('/', admin_dashboard),
])
Convention-Based Auto-DiscoveryΒΆ
Tatami automatically discovers and registers:
π Routers (from routers/)ΒΆ
Files with router classes are registered
Nested directories create sub-routes
Example: routers/admin/users.py β /admin/users
π§ Services (from services/)ΒΆ
Classes are available for dependency injection
Automatic singleton management
Constructor dependencies are resolved
π Middleware (from middleware/)ΒΆ
Middleware classes are registered globally
Order can be controlled with naming (01_auth.py, 02_cors.py)
π Static Files (from static/)ΒΆ
Automatically served at /static
No configuration needed
π Templates (from templates/)ΒΆ
Jinja2 environment auto-configured
Templates available in routers
Best PracticesΒΆ
π― Keep Routers ThinΒΆ
Routers should handle HTTP concerns only:
# β
Good - thin router
class Users(router('/users')):
def __init__(self, user_service: UserService):
self.user_service = user_service
@post('/')
def create_user(self, user: UserCreate):
return self.user_service.create(user)
# β Bad - fat router
class Users(router('/users')):
@post('/')
def create_user(self, user: UserCreate):
# Don't put business logic here!
if User.query.filter_by(email=user.email).first():
raise ValueError("Email exists")
# ... more business logic
π§ Put Logic in ServicesΒΆ
Services handle business rules and data access:
# β
Good - service handles business logic
from tatami.di import injectable
@injectable
class UserService:
def create_user(self, user_data: UserCreate):
# Validation
if self.user_exists(user_data.email):
raise UserAlreadyExistsError()
# Business rules
user_data = self.apply_business_rules(user_data)
# Data access
return self.user_repo.create(user_data)
π Use Pydantic ModelsΒΆ
Define clear data contracts:
# models.py or in router files
class UserCreate(BaseModel):
name: str = Field(min_length=1, max_length=100)
email: EmailStr
age: int = Field(ge=13, le=120)
class UserResponse(BaseModel):
id: int
name: str
email: str
created_at: datetime
π§ Configure ThoughtfullyΒΆ
Keep configuration clean and environment-specific:
# config.yaml - production defaults
database:
pool_size: 20
echo: false
features:
debug: false
enable_profiling: false
# config-dev.yaml - development overrides
database:
echo: true
features:
debug: true
enable_profiling: true
Why This Structure WorksΒΆ
π Rapid DevelopmentΒΆ
No boilerplate configuration
Auto-discovery reduces setup time
Clear separation of concerns
π§ͺ TestabilityΒΆ
Services can be unit tested easily
HTTP layer separated from business logic
Dependency injection enables mocking
π ScalabilityΒΆ
Structure remains clean as projects grow
Easy to split into microservices later
Clear boundaries between components
Real-World ExampleΒΆ
Hereβs how a real e-commerce API might be structured:
ecommerce-api/
βββ config.yaml
βββ routers/
β βββ products.py # Product catalog
β βββ users.py # User management
β βββ orders.py # Order processing
β βββ payments.py # Payment handling
β βββ admin/
β βββ analytics.py # Admin analytics
β βββ inventory.py # Inventory management
βββ services/
β βββ product_service.py # Product business logic
β βββ order_service.py # Order processing
β βββ payment_service.py # Payment integration
β βββ email_service.py # Email notifications
β βββ data/
β βββ product_repo.py # Product data access
β βββ order_repo.py # Order data access
βββ middleware/
β βββ auth_middleware.py # Authentication
β βββ rate_limit.py # Rate limiting
β βββ request_logging.py # Request logging
βββ static/
β βββ css/
β βββ js/
βββ templates/
βββ emails/ # Email templates
βββ admin/ # Admin interface
This structure scales from small APIs to large applications while maintaining clarity and organization.
Whatβs Next?ΒΆ
Now that you understand Tatamiβs project structure, youβre ready to:
Learn advanced routing patterns and techniques
Explore dependency injection for better code organization
Dive into middleware development
Master testing strategies for Tatami applications
The structure is your foundation - letβs build something amazing on it! ποΈ