← Back to DevBytes

Migrating from Legacy Frameworks to Sanic

What is Sanic?

Sanic is a Python 3.7+ web framework built for speed and asynchronous I/O. It uses the async/await syntax natively, runs on top of uvloop and httptools, and offers a Flask-like API that feels familiar to developers coming from synchronous frameworks. Unlike Flask or Django (in traditional WSGI mode), every request in Sanic is handled asynchronously, allowing thousands of concurrent connections without threading overhead.

Why Migrate from Legacy Frameworks?

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

Legacy frameworks such as Flask, Bottle, or Django’s WSGI server are synchronous by design. Each request occupies a thread or process, blocking the server while waiting for I/O operations like database queries, external API calls, or file reads. This model works well for low-to-medium traffic, but it becomes a bottleneck under high concurrency or when many requests spend time waiting.

Migrating to Sanic brings several concrete advantages:

The migration is not just about performance—it’s about future-proofing your stack for real-time features, microservices, and high-load APIs.

Migration Strategy: Step by Step

A successful migration involves adapting route handlers, middleware, error handlers, templates, and database access to the async model. The following sections walk you through each area with practical code comparisons.

1. Route Handlers: From Synchronous to Asynchronous

The most visible change is turning your view functions into async def coroutines. The request object in Sanic is similar to Flask’s request global, but it’s passed explicitly as the first argument.

Flask (legacy)

from flask import Flask, request
app = Flask(__name__)

@app.route('/hello')
def hello():
    name = request.args.get('name', 'World')
    return f'Hello, {name}!'

Sanic (migrated)

from sanic import Sanic
from sanic.response import text

app = Sanic("MyApp")

@app.route('/hello')
async def hello(request):
    name = request.args.get('name', 'World')
    return text(f'Hello, {name}!')

Key points: the handler becomes async def, the route decorator is identical, and text() is one of Sanic’s response helpers (others include json(), html(), file()).

2. Request and Response Handling

In Flask you access request data through global objects (request.json, request.args). Sanic keeps the same interface but the request object is passed as a parameter. JSON parsing is automatic, no need for await.

Flask – JSON endpoint

@app.route('/data', methods=['POST'])
def process_data():
    payload = request.get_json()
    result = payload['value'] * 2
    return {'result': result}

Sanic – JSON endpoint

@app.route('/data', methods=['POST'])
async def process_data(request):
    payload = request.json  # already parsed
    result = payload['value'] * 2
    return json({'result': result})

If you need to read raw body asynchronously (e.g., large uploads), use await request.body(). Sanic response objects are always returned synchronously—you don’t await them.

3. Middleware and Hooks

Flask uses before_request / after_request decorators. Sanic provides similar middleware but with two distinct stages: 'request' and 'response', both asynchronous.

Sanic middleware example

@app.middleware('request')
async def add_custom_context(request):
    # Runs before each handler
    request.ctx.user = "authenticated_user"

@app.middleware('response')
async def add_custom_header(request, response):
    # Runs before sending the response
    response.headers['X-User'] = request.ctx.user

You can also attach listeners for startup/shutdown events:

@app.listener('before_server_start')
async def setup_db(app, loop):
    app.ctx.db = await create_async_pool()

@app.listener('after_server_stop')
async def close_db(app, loop):
    await app.ctx.db.close()

4. Error Handling

Flask’s @app.errorhandler becomes Sanic’s @app.exception. Exceptions like NotFound, InvalidUsage, or custom ones are handled asynchronously.

from sanic.exceptions import NotFound, SanicException

@app.exception(NotFound)
async def custom_404(request, exception):
    return text("Page not found – custom message", status=404)

@app.exception(SanicException)
async def generic_handler(request, exception):
    return json({"error": str(exception)}, status=exception.status_code)

5. Blueprints (Modular Routing)

Both frameworks support blueprints for organizing larger applications. The migration is straightforward—just replace Flask’s Blueprint with Sanic’s blueprint and make handlers async.

from sanic import Blueprint

bp = Blueprint('users', url_prefix='/users')

@bp.route('/')
async def get_user(request, user_id):
    user = await fetch_user(user_id)
    return json(user)

app.blueprint(bp)

6. Template Rendering

Sanic does not bundle a template engine, but you can easily integrate Jinja2 via sanic-jinja2 or render templates asynchronously with aiofiles. A common pattern:

from sanic_jinja2 import SanicJinja2
jinja = SanicJinja2()

app = Sanic()
jinja.init_app(app)

@app.route('/profile')
async def profile(request):
    return jinja.render("profile.html", request, user=request.ctx.user)

For simpler HTML responses, use html() helper with an f-string or template string.

7. Database Access and Async Drivers

This is often the biggest change. Replace synchronous ORM/DB calls with async-compatible drivers:

Example with asyncpg and Sanic:

import asyncpg
from sanic import Sanic
from sanic.response import json

app = Sanic("AsyncDB")

@app.listener('before_server_start')
async def connect_db(app, loop):
    app.ctx.pool = await asyncpg.create_pool(dsn='postgresql://...')

@app.listener('after_server_stop')
async def disconnect_db(app, loop):
    await app.ctx.pool.close()

@app.route('/users/')
async def get_user(request, user_id):
    async with app.ctx.pool.acquire() as conn:
        row = await conn.fetchrow('SELECT * FROM users WHERE id = $1', user_id)
    return json(dict(row) if row else {})

Best Practices for a Smooth Migration

Conclusion

Migrating from a legacy synchronous framework to Sanic is a strategic step toward high-concurrency, modern Python web services. The transition follows a clear pattern: convert route handlers to async, adopt async database drivers, and restructure middleware and error handlers. While it requires careful planning—especially around blocking I/O—the performance gains and the ability to handle real-time features natively make it a worthwhile investment. Start small, benchmark frequently, and let Sanic’s familiar API guide you toward a faster, more scalable application.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles