Build REST APIs with Django REST Framework patterns in FastAPI
Transform your FastAPI development with familiar Django REST Framework patterns.
FastAPI Ronin gives FastAPI + Tortoise ORM projects a clean class-based API layer: ViewSets, explicit schemas, filters, pagination, permissions, response wrappers, custom actions, request state, and cache.
It is small enough to understand quickly, but structured enough to grow from a single-file prototype into a domain-oriented production app.
uv add fastapi-ronin fastapi tortoise-orm uvicornFor Redis-backed cache:
uv add "fastapi-ronin[redis]"The root main.py is a complete runnable application. It includes
database setup, a model, explicit schemas, filters, ordering, pagination,
response wrappers, cache, and a custom action.
from contextlib import asynccontextmanager
from datetime import datetime
from fastapi import APIRouter, FastAPI
from pydantic import BaseModel
from tortoise import fields
from tortoise.contrib.fastapi import register_tortoise
from tortoise.contrib.pydantic import PydanticModel
from tortoise.expressions import Q
from tortoise.models import Model
from tortoise.queryset import QuerySet
from fastapi_ronin.cache import cache
from fastapi_ronin.decorators import action, schema, viewset
from fastapi_ronin.filters import CharFilter, DateTimeFilter, FilterSet, OrderingFilter, Parameter
from fastapi_ronin.pagination import PageNumberPagination
from fastapi_ronin.viewsets import ModelViewSet
from fastapi_ronin.wrappers import PaginatedResponseDataWrapper, ResponseDataWrapper
def register_database(app: FastAPI):
register_tortoise(
app,
db_url='sqlite://db.sqlite3',
modules={'models': ['main']},
generate_schemas=True,
add_exception_handlers=True,
)
class Company(Model):
id = fields.IntField(primary_key=True)
name = fields.CharField(max_length=255)
full_name = fields.TextField(null=True)
created_at = fields.DatetimeField(auto_now_add=True)
updated_at = fields.DatetimeField(auto_now=True)
@schema(Company)
class CompanyCreateSchema(PydanticModel):
name: str
full_name: str | None
@schema(Company)
class CompanyReadSchema(CompanyCreateSchema):
id: int
created_at: datetime
updated_at: datetime
class StatsSchema(BaseModel):
total: int
called_cache: int = 0
class CompanyFilterSet(FilterSet):
fields = [
CharFilter(field_name='name', view_name='search_by_name', lookup_expr='icontains'),
CharFilter(field_name='search', method='filter_by_search'),
DateTimeFilter(field_name='created_at', lookups=['gte', 'lte', 'exact']),
DateTimeFilter(field_name='updated_at', lookups=['gte', 'lte', 'exact']),
]
ordering = OrderingFilter(
fields=(
'name',
('created', 'created_at'),
('updated', 'updated_at'),
),
default=('-created',),
)
def filter_by_search(self, queryset: QuerySet[Company], value: str, parameter: Parameter):
return queryset.filter(Q(name__icontains=value) | Q(full_name__icontains=value))
class Meta:
model = Company
router = APIRouter(prefix='/companies', tags=['companies'])
@viewset(router)
class CompanyViewSet(ModelViewSet[Company]):
model = Company
create_schema = CompanyCreateSchema
read_schema = CompanyReadSchema
pagination = PageNumberPagination
list_wrapper = PaginatedResponseDataWrapper
single_wrapper = ResponseDataWrapper
filterset_class = CompanyFilterSet
@action(methods=['GET'], detail=False)
async def stats(self) -> StatsSchema:
called = (await cache.get('stats:call') or 0) + 1
await cache.set('stats:call', called)
return StatsSchema(total=await Company.all().count(), called_cache=called)
@asynccontextmanager
async def lifespan(app: FastAPI):
await cache.init(None)
yield
await cache.close()
app = FastAPI(title='My API', lifespan=lifespan)
register_database(app)
app.include_router(router)Start server:
uvicorn main:app --reloadOpen http://127.0.0.1:8000/docs.
This creates the following endpoints:
GET /companies/- list companies with filters, ordering, pagination, and wrapper metadataPOST /companies/- create a new companyGET /companies/{item_id}/- retrieve a companyPUT /companies/{item_id}/- update a companyPATCH /companies/{item_id}/- partially update a companyDELETE /companies/{item_id}/- delete a companyGET /companies/stats/- custom cached stats endpoint
Example requests:
GET /companies/?search=acme
GET /companies/?search_by_name=corp
GET /companies/?created_at__gte=2026-01-01T00:00:00
GET /companies/?ordering=-updated
Example list response:
{
"data": [
{
"id": 1,
"name": "Acme Corp",
"full_name": "Acme Corporation Ltd.",
"created_at": "2026-01-01T10:00:00Z",
"updated_at": "2026-01-01T10:00:00Z"
}
],
"meta": {
"page": 1,
"size": 10,
"total": 47,
"pages": 5
}
}Example detail response:
{
"data": {
"id": 1,
"name": "Acme Corp",
"full_name": "Acme Corporation Ltd.",
"created_at": "2026-01-01T10:00:00Z",
"updated_at": "2026-01-01T10:00:00Z"
}
}Example custom action response:
{
"total": 123,
"called_cache": 4
}@schema(Model). Your API contract stays visible in code.
FastAPI Ronin is designed with these principles:
- Familiar: If you know Django REST Framework, ViewSets and permissions will feel natural.
- Explicit: Schemas are real Python classes, not hidden dynamic output.
- Flexible: Use only the pieces you need: ViewSets, filters, wrappers, permissions, cache, or state.
- Fast: Built on FastAPI, async Python, and Tortoise ORM.
- Modular: Start in one file, then move to a domain architecture when the app grows.
Ready to build a scalable project layout? Start with the Quick Start guide.
Want to dive deeper?
- ViewSets - core ViewSet concepts
- Schemas - explicit request and response models
- Filters - query parameters and lookup expressions
- Cache - in-memory and Redis-backed cache
- Permissions - authentication-aware access rules
- Pagination - page-number and limit-offset pagination
- State Management - request-scoped state
- Response Wrappers - consistent API responses
FastAPI Ronin is open source. Issues, ideas, documentation improvements, and pull requests all help shape the library.
- GitHub: github.com/bubaley/fastapi-ronin
- Issues: Report bugs or request features
- Discussions: Ask questions and share patterns
FastAPI Ronin is released under the MIT License.
