Migrating from FastAPI βš‘βž‘οΈπŸŽ‹ΒΆ

This guide helps FastAPI developers transition to Tatami, highlighting the similarities and unique advantages of Tatami’s approach.

Why Consider Tatami?ΒΆ

As a FastAPI developer, you’ll appreciate Tatami’s:

  • βœ… Similar philosophy - Type hints and automatic validation

  • βœ… Better organization - Class-based routers group related endpoints

  • βœ… Cleaner structure - Convention-based project organization

  • βœ… Less boilerplate - Smart defaults reduce configuration

  • βœ… Explicit routing - Clear, obvious API definitions

  • βœ… Modular design - Built-in separation of concerns

Key SimilaritiesΒΆ

Both frameworks share:

  • ASGI-based for async support

  • Pydantic integration for data validation

  • Automatic OpenAPI documentation generation

  • Type hint support throughout

  • Modern Python patterns (3.8+)

Key DifferencesΒΆ

FastAPI vs Tatami ComparisonΒΆ

Feature

FastAPI

Tatami

Routing Style

Function decorators

Class-based routers

Project Structure

Flexible (manual setup)

Convention-based discovery

Organization

Single file or manual modules

Clear separation by design

Dependency Injection

Manual Depends() everywhere

Auto-discovery and injection

Configuration

Code-based or manual

YAML-based with modes

Basic FastAPI to Tatami TranslationΒΆ

FastAPI App StructureΒΆ

# FastAPI main.py
from fastapi import FastAPI, Depends, HTTPException
from pydantic import BaseModel
from typing import List

app = FastAPI(title="My API")

class User(BaseModel):
    name: str
    email: str

class UserCreate(BaseModel):
    name: str
    email: str

# In-memory storage
users_db = []

@app.get("/users", response_model=List[User])
def get_users():
    return users_db

@app.post("/users", response_model=User)
def create_user(user: UserCreate):
    new_user = User(**user.dict())
    users_db.append(new_user)
    return new_user

@app.get("/users/{user_id}", response_model=User)
def get_user(user_id: int):
    if user_id >= len(users_db):
        raise HTTPException(status_code=404, detail="User not found")
    return users_db[user_id]

Equivalent Tatami CodeΒΆ

# routers/users.py
from tatami import router, get, post
from pydantic import BaseModel
from typing import List

class User(BaseModel):
    name: str
    email: str

class UserCreate(BaseModel):
    name: str
    email: str

class Users(router('/users')):
    """User management endpoints"""

    def __init__(self):
        super().__init__()
        self.users_db = []

    @get('/')
    def get_users(self) -> List[User]:
        """Get all users"""
        return self.users_db

    @post('/')
    def create_user(self, user: UserCreate) -> User:
        """Create a new user"""
        new_user = User(**user.dict())
        self.users_db.append(new_user)
        return new_user

    @get('/{user_id}')
    def get_user(self, user_id: int) -> User:
        """Get user by ID"""
        if user_id >= len(self.users_db):
            return {'error': 'User not found'}, 404
        return self.users_db[user_id]

Key improvements: - βœ… Related endpoints grouped in a class - βœ… Self-contained state management - βœ… Auto-discovery when using tatami run - βœ… Cleaner project organization

Migration StrategiesΒΆ

1. Incremental MigrationΒΆ

Migrate FastAPI routers one by one:

Step 1: Convert a FastAPI router to Tatami

# FastAPI router
from fastapi import APIRouter

router = APIRouter(prefix="/posts")

@router.get("/")
def get_posts():
    return []

@router.post("/")
def create_post(post: PostCreate):
    return {"message": "Post created"}
# Tatami router
from tatami import router, get, post

class Posts(router('/posts')):
    @get('/')
    def get_posts(self):
        return []

    @post('/')
    def create_post(self, post: PostCreate):
        return {"message": "Post created"}

Step 2: Migrate Dependencies

# FastAPI dependencies
from fastapi import Depends

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

@app.get("/users")
def get_users(db: Session = Depends(get_db)):
    return crud.get_users(db)
# Tatami with service injection
class Users(router('/users')):
    def __init__(self, user_service: UserService):
        super().__init__()
        self.user_service = user_service

    @get('/')
    def get_users(self):
        return self.user_service.get_all()

2. Complete RewriteΒΆ

For clean architecture, consider a complete rewrite:

# Create new Tatami project
tatami create my-migrated-api
cd my-migrated-api

# Convert FastAPI routes to Tatami routers
# Organize code into services and repositories
# Update configuration to use YAML

Common Migration PatternsΒΆ

FastAPI Path Parameters β†’ Tatami Path ParametersΒΆ

# FastAPI
@app.get("/users/{user_id}/posts/{post_id}")
def get_user_post(user_id: int, post_id: int):
    return {"user_id": user_id, "post_id": post_id}
# Tatami
class UserPosts(router('/users/{user_id}/posts')):
    @get('/{post_id}')
    def get_user_post(self, user_id: int, post_id: int):
        return {"user_id": user_id, "post_id": post_id}

FastAPI Query Parameters β†’ Tatami Query ParametersΒΆ

# FastAPI
from fastapi import Query

@app.get("/search")
def search(
    q: str = Query(...),
    page: int = Query(1),
    limit: int = Query(10)
):
    return {"query": q, "page": page, "limit": limit}
# Tatami
from tatami.param import Query

class Search(router('/search')):
    @get('/')
    def search(
        self,
        q: str = Query(...),
        page: int = Query(1),
        limit: int = Query(10)
    ):
        return {"query": q, "page": page, "limit": limit}

FastAPI Dependencies β†’ Tatami Dependency InjectionΒΆ

# FastAPI
from fastapi import Depends

def get_user_service():
    return UserService()

def get_email_service():
    return EmailService()

@app.post("/users")
def create_user(
    user: UserCreate,
    user_service: UserService = Depends(get_user_service),
    email_service: EmailService = Depends(get_email_service)
):
    new_user = user_service.create(user)
    email_service.send_welcome(new_user.email)
    return new_user
# Tatami
class Users(router('/users')):
    def __init__(self, user_service: UserService, email_service: EmailService):
        super().__init__()
        self.user_service = user_service
        self.email_service = email_service

    @post('/')
    def create_user(self, user: UserCreate):
        new_user = self.user_service.create(user)
        self.email_service.send_welcome(new_user.email)
        return new_user

FastAPI Middleware β†’ Tatami MiddlewareΒΆ

# FastAPI
from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)
# Tatami
# middleware/cors_middleware.py
from starlette.middleware.cors import CORSMiddleware

# Auto-discovered and applied globally

FastAPI Background Tasks β†’ Tatami ServicesΒΆ

# FastAPI
from fastapi import BackgroundTasks

def send_email(email: str, message: str):
    # Send email logic
    pass

@app.post("/send-email")
def send_email_endpoint(
    email: str,
    message: str,
    background_tasks: BackgroundTasks
):
    background_tasks.add_task(send_email, email, message)
    return {"message": "Email will be sent"}
# Tatami
class Email(router('/email')):
    def __init__(self, email_service: EmailService):
        super().__init__()
        self.email_service = email_service

    @post('/send')
    async def send_email(self, email: str, message: str):
        # Use asyncio or celery for background processing
        await self.email_service.send_async(email, message)
        return {"message": "Email sent"}

Configuration MigrationΒΆ

FastAPI SettingsΒΆ

# FastAPI settings.py
from pydantic import BaseSettings

class Settings(BaseSettings):
    app_name: str = "My API"
    database_url: str = "sqlite:///./app.db"
    secret_key: str

    class Config:
        env_file = ".env"

settings = Settings()

Tatami ConfigurationΒΆ

# config.yaml
app:
  name: "My API"
  secret_key: "${SECRET_KEY}"

database:
  url: "sqlite:///./app.db"
# config-dev.yaml
database:
  url: "sqlite:///./dev.db"

features:
  debug: true

Testing MigrationΒΆ

FastAPI TestingΒΆ

# FastAPI testing
from fastapi.testclient import TestClient

client = TestClient(app)

def test_get_users():
    response = client.get("/users")
    assert response.status_code == 200
    assert response.json() == []

Tatami TestingΒΆ

# Tatami testing
import httpx
from tatami import BaseRouter

def test_get_users():
    app = BaseRouter()
    app.include_router(Users())

    with httpx.Client(app=app, base_url="http://test") as client:
        response = client.get("/users")
        assert response.status_code == 200
        assert response.json() == []

Project Structure ComparisonΒΆ

FastAPI Project StructureΒΆ

fastapi-project/
β”œβ”€β”€ main.py              # All routes or app setup
β”œβ”€β”€ models.py            # Pydantic models
β”œβ”€β”€ database.py          # Database setup
β”œβ”€β”€ crud.py              # Database operations
β”œβ”€β”€ dependencies.py      # Dependency functions
β”œβ”€β”€ routers/             # Optional organization
β”‚   β”œβ”€β”€ users.py
β”‚   └── posts.py
└── requirements.txt

Tatami Project StructureΒΆ

tatami-project/
β”œβ”€β”€ config.yaml          # Configuration
β”œβ”€β”€ routers/             # API endpoints (auto-discovered)
β”‚   β”œβ”€β”€ users.py
β”‚   └── posts.py
β”œβ”€β”€ services/            # Business logic (auto-discovered)
β”‚   β”œβ”€β”€ user_service.py
β”‚   └── email_service.py
β”œβ”€β”€ repositories/        # Data access (auto-discovered)
β”‚   └── user_repository.py
β”œβ”€β”€ middleware/          # Middleware (auto-discovered)
β”‚   └── auth_middleware.py
β”œβ”€β”€ static/              # Static files (auto-served)
└── templates/           # Templates (auto-configured)

Advantages of MigrationΒΆ

Better OrganizationΒΆ

FastAPI encourages but doesn’t enforce good structure. Tatami provides it by default:

# FastAPI - everything in one file (common but not ideal)
from fastapi import FastAPI

app = FastAPI()

# 50+ route definitions here...
# Models scattered throughout...
# Business logic mixed with HTTP concerns...
# Tatami - clear separation by convention
# routers/users.py - HTTP concerns only
# services/user_service.py - Business logic
# repositories/user_repository.py - Data access

Reduced BoilerplateΒΆ

# FastAPI - manual dependency injection everywhere
@app.get("/users")
def get_users(
    user_service: UserService = Depends(get_user_service),
    email_service: EmailService = Depends(get_email_service)
):
    pass
# Tatami - inject once in constructor
class Users(router('/users')):
    def __init__(self, user_service: UserService, email_service: EmailService):
        self.user_service = user_service
        self.email_service = email_service

Auto-DiscoveryΒΆ

# FastAPI - manual registration
app.include_router(users_router)
app.include_router(posts_router)
app.include_router(auth_router)
# ... manual setup for everything
# Tatami - automatic discovery
tatami run .  # Discovers everything automatically

Migration GotchasΒΆ

1. Dependency Injection DifferencesΒΆ

FastAPI uses function-level dependency injection:

# FastAPI
@app.get("/users")
def get_users(db: Session = Depends(get_db)):
    pass

Tatami uses constructor-level injection:

# Tatami
class Users(router('/users')):
    def __init__(self, user_service: UserService):
        self.user_service = user_service

2. Response ModelsΒΆ

FastAPI uses response_model parameter:

# FastAPI
@app.get("/users", response_model=List[User])
def get_users():
    pass

Tatami uses return type hints:

# Tatami
@get('/')
def get_users(self) -> List[User]:
    pass

3. Exception HandlingΒΆ

FastAPI uses HTTPException:

# FastAPI
from fastapi import HTTPException

@app.get("/users/{user_id}")
def get_user(user_id: int):
    if user_id not in users:
        raise HTTPException(status_code=404, detail="User not found")

Tatami uses tuple returns:

# Tatami
@get('/{user_id}')
def get_user(self, user_id: int):
    if user_id not in self.users:
        return {"error": "User not found"}, 404

Migration ChecklistΒΆ

Before MigrationΒΆ

  • [ ] Audit FastAPI app - Catalog all routes, dependencies, middleware

  • [ ] Identify business logic - What can be moved to services?

  • [ ] Review dependencies - How are services currently injected?

  • [ ] Document current structure - Understand existing organization

During MigrationΒΆ

  • [ ] Create Tatami project - tatami create new-app

  • [ ] Convert routes to routers - Group related endpoints

  • [ ] Extract services - Move business logic to service classes

  • [ ] Update dependencies - Use constructor injection

  • [ ] Migrate configuration - Convert to YAML format

After MigrationΒΆ

  • [ ] Test thoroughly - Ensure all functionality works

  • [ ] Verify auto-discovery - Check tatami doctor

  • [ ] Update deployment - Use tatami run for serving

  • [ ] Document changes - API docs are auto-generated

Why Make the Switch?ΒΆ

While FastAPI is excellent, Tatami offers:

  • πŸ—οΈ Better Architecture - Enforced separation of concerns

  • πŸ”§ Less Configuration - Convention over configuration approach

  • πŸ“ Cleaner Projects - Organized structure from day one

  • πŸš€ Faster Development - Auto-discovery reduces boilerplate

  • πŸ§ͺ Better Testing - Clear dependencies make mocking easier

The migration effort pays off in maintainability and team productivity! πŸŽ‹