If you’ve worked with Django, you know the layout: apps/, models.py, views.py, urls.py. It’s opinionated, but the opinions are good. When a new developer joins a Django project, they already know roughly where everything lives.
I wanted that same predictability with an async-native Python web framework. Django’s ORM is synchronous at its core (async support exists but is a retrofit), and that matters when you’re building something IO-heavy.
Sjango is the result — a minimal boilerplate that maps Django’s structural conventions onto Sanic.
The stack
- Sanic for the async HTTP server (Python’s fastest async web framework)
- Tortoise ORM for async database access with a Django-like model API
- Pydantic Settings for configuration via environment variables
- sanic-ext for Jinja2 template rendering
- Blueprint-based routing with
path()andinclude()helpers that mirror Django’surlpatterns
The project structure
app.py # Sanic entry point
urls.py # Root URL configuration
core/
settings.py # Pydantic-based settings
routing.py # path() / include() helpers
middleware.py # Middleware loader
apps/
users/ # model, views, urls
blog/ # model, views, urls
payments/ # model, views, urls
templates/ # Jinja2 templates
Adding a new app is exactly what a Django developer would expect: create the directory, define urlpatterns in urls.py, register it in the root urls.py with include(), and add it to INSTALLED_APPS so Tortoise picks up its models.
Why I built it
Partly frustration: every “Sanic starter” I found online was either a single app.py file with everything in it, or a massively over-engineered template with Docker, CI/CD, authentication, caching, and seventeen other things I didn’t need.
Partly because I wanted to understand Sanic better. Building the routing helpers (path() and include()) from scratch forced me to read Sanic’s blueprint and router APIs carefully, and I came away with a much better mental model of how Sanic handles routing internally.
The routing layer
Django’s path() function returns a route object; include() mounts a list of those routes under a prefix. Reproducing this in Sanic required wrapping Sanic’s Blueprint in a thin API:
# core/routing.py
def path(url, view, name=None):
return (url, view, name)
def include(prefix, patterns):
bp = Blueprint(prefix, url_prefix=prefix)
for url, view, name in patterns:
bp.add_route(view.as_view(), url, name=name)
return bp
Then the root urls.py just collects blueprints and attaches them to the Sanic app. It’s a thin wrapper, but it makes the intent clear.
Class-based views
Django’s class-based views are a love-it-or-hate-it feature. I included a minimal BaseView that maps HTTP methods to handler methods:
class UserListView(BaseView):
async def get(self, request):
users = await User.all()
return json([u.to_dict() for u in users])
No magic beyond method dispatch — none of Django’s get_queryset, get_context_data, mixins. Just a clean base class that routes GET, POST, etc. to the right method.
Middleware
Django’s middleware is a list of class paths in settings.py. I replicated that: MIDDLEWARE is a list of dotted import paths to async functions. At startup, the middleware loader imports each one and attaches it to the Sanic app. Adding middleware to a new project is a one-line change in settings.
What’s missing (intentionally)
There’s no authentication system, no admin interface, no form library. Those are real framework features — Sjango is a boilerplate, not a framework. It gives you the structure and wires together the tools; you bring the application logic.
Try it
The project is on GitHub. Clone it, install dependencies, copy .env.example to .env, and run python app.py. You’ll have a working Sanic server with a users app, a blog app, and a payments app, all wired up via the Django-style routing layer.