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 name

  • app_version - Application version

  • openapi_spec - The OpenAPI specification object

  • base_url - Base URL of your API

  • docs_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

🧭 Easy Navigation¢

  • Predictable file locations

  • Related code stays together

  • New developers onboard quickly

πŸ§ͺ 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! πŸ—οΈ