Migrating from Django πΈβ‘οΈπΒΆ
This guide helps Django developers transition to Tatami, showing how Djangoβs MTV pattern translates to Tatamiβs modern API architecture.
Why Consider Tatami?ΒΆ
As a Django developer, youβll find Tatami offers:
β API-first design - Built specifically for modern web APIs
β Simpler deployment - No complex WSGI/ASGI configuration
β Auto-discovery - Automatic component registration like Django apps
β Type safety - Full type hint support throughout
β Modern async - Built on ASGI from the ground up
β Less boilerplate - Convention over configuration
Key Concept MappingΒΆ
Django Concept |
Tatami Equivalent |
Purpose |
---|---|---|
Views (Class-based) |
Class-based Routers |
Handle HTTP requests |
URL patterns |
Router decorators |
Define endpoints |
Models |
Pydantic models |
Data validation/serialization |
Services/Managers |
Service classes |
Business logic |
Middleware |
Middleware classes |
Request/response processing |
Apps |
Router modules |
Organize functionality |
Settings |
YAML configuration |
App configuration |
Basic Django to Tatami TranslationΒΆ
Django Views β Tatami RoutersΒΆ
Django Class-Based Views:
# Django views.py
from django.http import JsonResponse
from django.views import View
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
from .models import Post
from .serializers import PostSerializer
@method_decorator(csrf_exempt, name='dispatch')
class PostView(View):
def get(self, request, post_id=None):
if post_id:
try:
post = Post.objects.get(id=post_id)
return JsonResponse(PostSerializer(post).data)
except Post.DoesNotExist:
return JsonResponse({'error': 'Post not found'}, status=404)
else:
posts = Post.objects.all()
return JsonResponse([PostSerializer(p).data for p in posts], safe=False)
def post(self, request):
serializer = PostSerializer(data=request.POST)
if serializer.is_valid():
post = serializer.save()
return JsonResponse(PostSerializer(post).data, status=201)
return JsonResponse(serializer.errors, status=400)
Equivalent Tatami Router:
# routers/posts.py
from tatami import router, get, post
from pydantic import BaseModel
from typing import List, Optional
class Post(BaseModel):
id: Optional[int] = None
title: str
content: str
author: str
class PostCreate(BaseModel):
title: str
content: str
author: str
class Posts(router('/posts')):
"""Post management endpoints"""
def __init__(self, post_service: PostService):
super().__init__()
self.post_service = post_service
@get('/')
def list_posts(self) -> List[Post]:
"""Get all posts"""
return self.post_service.get_all()
@get('/{post_id}')
def get_post(self, post_id: int) -> Post:
"""Get post by ID"""
post = self.post_service.get_by_id(post_id)
if not post:
return {'error': 'Post not found'}, 404
return post
@post('/')
def create_post(self, post_data: PostCreate) -> Post:
"""Create a new post"""
return self.post_service.create(post_data)
Django URLs β Tatami RoutingΒΆ
Django URL Configuration:
# urls.py
from django.urls import path, include
from . import views
app_name = 'blog'
urlpatterns = [
path('posts/', views.PostView.as_view(), name='post-list'),
path('posts/<int:post_id>/', views.PostView.as_view(), name='post-detail'),
path('users/', include('users.urls')),
path('comments/', include('comments.urls')),
]
# Main urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('api/v1/', include('blog.urls')),
]
Tatami Auto-Discovery:
# routers/posts.py - automatically discovered
class Posts(router('/api/v1/posts')):
# Routes automatically registered
pass
# routers/users.py - automatically discovered
class Users(router('/api/v1/users')):
# Routes automatically registered
pass
# routers/comments.py - automatically discovered
class Comments(router('/api/v1/comments')):
# Routes automatically registered
pass
Django Models β Pydantic Models + ServicesΒΆ
Django Models:
# models.py
from django.db import models
from django.contrib.auth.models import User
class Post(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
author = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['-created_at']
def __str__(self):
return self.title
@classmethod
def get_by_author(cls, author):
return cls.objects.filter(author=author)
Tatami Pydantic Models + Service:
# models.py (Pydantic models for API)
from pydantic import BaseModel
from datetime import datetime
from typing import Optional
class Post(BaseModel):
id: Optional[int] = None
title: str
content: str
author_id: int
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None
class PostCreate(BaseModel):
title: str
content: str
author_id: int
# services/post_service.py (Business logic)
from typing import List, Optional
class PostService:
def __init__(self, post_repository: PostRepository):
self.post_repository = post_repository
def get_all(self) -> List[Post]:
return self.post_repository.find_all()
def get_by_id(self, post_id: int) -> Optional[Post]:
return self.post_repository.find_by_id(post_id)
def get_by_author(self, author_id: int) -> List[Post]:
return self.post_repository.find_by_author(author_id)
def create(self, post_data: PostCreate) -> Post:
return self.post_repository.create(post_data)
Django Forms/Serializers β Pydantic ValidationΒΆ
Django Forms/Serializers:
# forms.py or serializers.py
from django import forms
from rest_framework import serializers
from .models import Post
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ['title', 'content']
def clean_title(self):
title = self.cleaned_data['title']
if len(title) < 5:
raise forms.ValidationError("Title must be at least 5 characters")
return title
# Or with DRF
class PostSerializer(serializers.ModelSerializer):
class Meta:
model = Post
fields = ['id', 'title', 'content', 'author', 'created_at']
def validate_title(self, value):
if len(value) < 5:
raise serializers.ValidationError("Title must be at least 5 characters")
return value
Tatami Pydantic Validation:
# models.py
from pydantic import BaseModel, validator
from typing import Optional
class PostCreate(BaseModel):
title: str
content: str
author_id: int
@validator('title')
def title_must_be_long_enough(cls, v):
if len(v) < 5:
raise ValueError('Title must be at least 5 characters')
return v
@validator('content')
def content_not_empty(cls, v):
if not v.strip():
raise ValueError('Content cannot be empty')
return v
Migration StrategiesΒΆ
1. API-First Migration (Recommended)ΒΆ
Extract Django API functionality into Tatami:
Step 1: Identify API endpoints
# Django - mixed web + API
# views.py
def post_list_html(request): # Web view
return render(request, 'posts.html')
def post_list_api(request): # API view
posts = Post.objects.all()
return JsonResponse([serialize(p) for p in posts])
Step 2: Extract to Tatami API
# Tatami - pure API
class Posts(router('/api/posts')):
@get('/')
def list_posts(self) -> List[Post]:
return self.post_service.get_all()
Step 3: Update Django to use Tatami API
# Django - consume Tatami API
import httpx
def post_list_html(request):
response = httpx.get('http://api.example.com/api/posts')
posts = response.json()
return render(request, 'posts.html', {'posts': posts})
2. Gradual Router MigrationΒΆ
Migrate Django apps one by one:
# Django app structure
myproject/
βββ blog/ # Migrate first
βββ users/ # Migrate second
βββ comments/ # Migrate third
βββ notifications/ # Migrate last
# Tatami equivalent
tatami-api/
βββ routers/
β βββ blog.py # Migrated from blog app
β βββ users.py # Migrated from users app
β βββ comments.py # Migrated from comments app
βββ services/ # Business logic extracted
3. Microservice SplitΒΆ
Use Tatami for new microservices:
# Keep Django for web frontend
django-web/
βββ templates/
βββ static/
βββ views.py # Renders HTML, calls APIs
# New Tatami API services
tatami-api/
βββ user-service/ # User management
βββ post-service/ # Content management
βββ auth-service/ # Authentication
Common Migration PatternsΒΆ
Django Admin β Custom Admin InterfaceΒΆ
Django Admin:
# admin.py
from django.contrib import admin
from .models import Post
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_display = ['title', 'author', 'created_at']
list_filter = ['author', 'created_at']
search_fields = ['title', 'content']
Tatami Admin API:
# routers/admin.py
class Admin(router('/admin')):
def __init__(self, post_service: PostService):
self.post_service = post_service
@get('/posts')
def list_posts(
self,
author: Optional[str] = None,
search: Optional[str] = None
) -> List[Post]:
return self.post_service.admin_list(author, search)
Django Authentication β JWT/Custom AuthΒΆ
Django Authentication:
# Django views
from django.contrib.auth.decorators import login_required
@login_required
def protected_view(request):
return JsonResponse({'user': request.user.username})
Tatami Authentication:
# middleware/auth_middleware.py
from starlette.middleware.base import BaseHTTPMiddleware
class AuthMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
# JWT validation logic
response = await call_next(request)
return response
# routers/protected.py
class Protected(router('/protected')):
def __init__(self, auth_service: AuthService):
self.auth_service = auth_service
@get('/')
def protected_endpoint(self, request) -> dict:
user = self.auth_service.get_current_user(request)
return {'user': user.username}
Django Signals β Event ServicesΒΆ
Django Signals:
# signals.py
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import Post
@receiver(post_save, sender=Post)
def post_created(sender, instance, created, **kwargs):
if created:
send_notification(instance.author, f"Post '{instance.title}' created")
Tatami Event Services:
# services/post_service.py
class PostService:
def __init__(self, notification_service: NotificationService):
self.notification_service = notification_service
def create_post(self, post_data: PostCreate) -> Post:
post = self.post_repository.create(post_data)
# Explicit event handling
self.notification_service.send_post_created(post)
return post
Django Middleware β Tatami MiddlewareΒΆ
Django Middleware:
# middleware.py
class CustomMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# Process request
request.custom_header = request.META.get('HTTP_X_CUSTOM')
response = self.get_response(request)
# Process response
response['X-Custom-Response'] = 'processed'
return response
Tatami Middleware:
# middleware/custom_middleware.py
from starlette.middleware.base import BaseHTTPMiddleware
class CustomMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
# Process request
custom_header = request.headers.get('x-custom')
request.state.custom_data = custom_header
response = await call_next(request)
# Process response
response.headers['x-custom-response'] = 'processed'
return response
Configuration MigrationΒΆ
Django Settings β Tatami ConfigΒΆ
Django settings.py:
# settings.py
import os
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = os.environ.get('SECRET_KEY')
DEBUG = os.environ.get('DEBUG', 'False').lower() == 'true'
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.environ.get('DB_NAME'),
'USER': os.environ.get('DB_USER'),
'PASSWORD': os.environ.get('DB_PASSWORD'),
'HOST': os.environ.get('DB_HOST', 'localhost'),
'PORT': os.environ.get('DB_PORT', '5432'),
}
}
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'rest_framework',
'blog',
'users',
]
Tatami config.yaml:
# config.yaml
app:
name: "My API"
secret_key: "${SECRET_KEY}"
debug: false
database:
engine: "postgresql"
name: "${DB_NAME}"
user: "${DB_USER}"
password: "${DB_PASSWORD}"
host: "${DB_HOST:localhost}"
port: "${DB_PORT:5432}"
# config-dev.yaml
app:
debug: true
database:
host: "localhost"
name: "dev_db"
Database MigrationΒΆ
Django ORM β SQLAlchemy (Recommended)ΒΆ
Django Models:
# models.py
from django.db import models
class Post(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
author = models.ForeignKey('auth.User', on_delete=models.CASCADE)
tags = models.ManyToManyField('Tag', blank=True)
created_at = models.DateTimeField(auto_now_add=True)
SQLAlchemy Models:
# repositories/models.py
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
Base = declarative_base()
class PostModel(Base):
__tablename__ = 'posts'
id = Column(Integer, primary_key=True)
title = Column(String(200), nullable=False)
content = Column(Text, nullable=False)
author_id = Column(Integer, ForeignKey('users.id'))
created_at = Column(DateTime, nullable=False)
author = relationship("UserModel", back_populates="posts")
Repository PatternΒΆ
# repositories/post_repository.py
from typing import List, Optional
from .models import PostModel
class PostRepository:
def __init__(self, db_session):
self.db = db_session
def find_all(self) -> List[Post]:
models = self.db.query(PostModel).all()
return [Post.from_orm(m) for m in models]
def find_by_id(self, post_id: int) -> Optional[Post]:
model = self.db.query(PostModel).get(post_id)
return Post.from_orm(model) if model else None
def create(self, post_data: PostCreate) -> Post:
model = PostModel(**post_data.dict())
self.db.add(model)
self.db.commit()
return Post.from_orm(model)
Testing MigrationΒΆ
Django Tests β Tatami TestsΒΆ
Django Tests:
# tests.py
from django.test import TestCase, Client
from django.contrib.auth.models import User
from .models import Post
class PostTestCase(TestCase):
def setUp(self):
self.user = User.objects.create_user('testuser', 'test@example.com')
self.client = Client()
def test_create_post(self):
response = self.client.post('/api/posts/', {
'title': 'Test Post',
'content': 'Test content',
'author': self.user.id
})
self.assertEqual(response.status_code, 201)
self.assertEqual(Post.objects.count(), 1)
Tatami Tests:
# tests/test_posts.py
import httpx
import pytest
from tatami import BaseRouter
from routers.posts import Posts
from services.post_service import PostService
def test_create_post():
# Mock dependencies
mock_service = Mock(spec=PostService)
mock_service.create.return_value = Post(
id=1, title="Test Post", content="Test content"
)
# Setup app
app = BaseRouter()
app.include_router(Posts(mock_service))
# Test request
with httpx.Client(app=app, base_url="http://test") as client:
response = client.post("/posts/", json={
'title': 'Test Post',
'content': 'Test content',
'author_id': 1
})
assert response.status_code == 201
assert response.json()['title'] == 'Test Post'
Project Structure ComparisonΒΆ
Django Project StructureΒΆ
django-project/
βββ manage.py
βββ requirements.txt
βββ myproject/
β βββ __init__.py
β βββ settings.py
β βββ urls.py
β βββ wsgi.py
βββ blog/ # Django app
β βββ __init__.py
β βββ admin.py
β βββ apps.py
β βββ models.py
β βββ views.py
β βββ urls.py
β βββ serializers.py
β βββ migrations/
βββ users/ # Django app
β βββ models.py
β βββ views.py
β βββ urls.py
βββ static/
βββ templates/
Tatami Project StructureΒΆ
tatami-project/
βββ config.yaml # Configuration
βββ routers/ # API endpoints (auto-discovered)
β βββ posts.py # Blog functionality
β βββ users.py # User management
β βββ auth.py # Authentication
βββ services/ # Business logic (auto-discovered)
β βββ post_service.py # Post operations
β βββ user_service.py # User operations
β βββ auth_service.py # Authentication logic
βββ repositories/ # Data access (auto-discovered)
β βββ post_repository.py # Post data access
β βββ user_repository.py # User data access
βββ middleware/ # Middleware (auto-discovered)
β βββ auth_middleware.py # Authentication
β βββ cors_middleware.py # CORS handling
βββ models/ # Pydantic models
β βββ post.py
β βββ user.py
βββ static/ # Static files (auto-served)
Migration AdvantagesΒΆ
PerformanceΒΆ
Django: - Synchronous by default - Complex async configuration - ORM can generate inefficient queries
Tatami: - Async-first architecture - Built on Starlette/ASGI - Direct control over database queries
Development SpeedΒΆ
Django:
# Multiple files for simple CRUD
# models.py
class Post(models.Model): pass
# serializers.py
class PostSerializer(serializers.ModelSerializer): pass
# views.py
class PostViewSet(viewsets.ModelViewSet): pass
# urls.py
router.register(r'posts', PostViewSet)
Tatami:
# Single file for simple CRUD
class Posts(router('/posts')):
@get('/')
def list_posts(self) -> List[Post]: pass
@post('/')
def create_post(self, post: PostCreate) -> Post: pass
Type SafetyΒΆ
Django: Limited type hints, runtime errors
def get_posts(request):
# No type hints for request/response
posts = Post.objects.all()
# Serialization can fail at runtime
return JsonResponse([serialize(p) for p in posts])
Tatami: Full type safety throughout
@get('/')
def list_posts(self) -> List[Post]: # Return type enforced
return self.post_service.get_all() # Validated automatically
Migration GotchasΒΆ
1. No Built-in AdminΒΆ
Django Admin doesnβt translate directly. Consider:
Build custom admin interface
Use existing admin tools (Django Admin + API calls)
Third-party admin solutions
2. Different Database PatternsΒΆ
Django uses Active Record pattern, Tatami uses Repository pattern:
# Django Active Record
post = Post.objects.get(id=1)
post.title = "Updated"
post.save()
# Tatami Repository Pattern
post = self.post_repository.find_by_id(1)
updated_post = post.copy(update={'title': 'Updated'})
self.post_repository.save(updated_post)
3. Authentication DifferencesΒΆ
Django has built-in user model and session auth. Tatami typically uses JWT:
# Django - built-in sessions
if request.user.is_authenticated:
pass
# Tatami - JWT/custom auth
user = self.auth_service.verify_token(request.headers.get('authorization'))
Migration ChecklistΒΆ
Planning PhaseΒΆ
[ ] Audit Django project - Identify API vs web functionality
[ ] Map Django apps - Plan Tatami router organization
[ ] Review models - Plan Pydantic model structure
[ ] Identify dependencies - What services need injection?
[ ] Plan database migration - ORM to Repository pattern
Implementation PhaseΒΆ
[ ] Create Tatami project - tatami create new-api
[ ] Convert models - Django models to Pydantic + SQLAlchemy
[ ] Migrate views - Django views to Tatami routers
[ ] Extract services - Business logic to service classes
[ ] Setup repositories - Data access layer
[ ] Convert middleware - Django to Starlette middleware
Testing PhaseΒΆ
[ ] Port tests - Django tests to Tatami tests
[ ] Test endpoints - Verify API functionality
[ ] Performance testing - Compare response times
[ ] Integration testing - Test with existing Django frontend
Deployment PhaseΒΆ
[ ] Update CI/CD - Use tatami run for deployment
[ ] Configure load balancer - Route to Tatami API
[ ] Monitor performance - Track API metrics
[ ] Plan rollback - Keep Django API as backup
Why Migrate from Django?ΒΆ
For API-focused projects, Tatami offers:
π Better Performance - Async-first architecture
π§Ή Cleaner Code - Less boilerplate, better organization
β‘ Faster Development - Convention over configuration
π Type Safety - Catch errors at development time
π¦ Modern Patterns - Built for current Python practices
π οΈ Better Testing - Clear dependencies, easy mocking
The migration effort creates a more maintainable, performant API! π
Note: Keep Django for admin interfaces and complex web apps. Use Tatami for clean, fast APIs.