← Back to DevBytes

Sanic from Scratch: Practical Guide to

Introduction to Sanic

Sanic is a modern, asynchronous Python web framework built on top of asyncio. It was created with one primary goal in mind: speed. Inspired by Flask's developer-friendly API but designed for high-performance production workloads, Sanic allows you to write HTTP servers that can handle thousands of requests per second on a single process. Unlike traditional WSGI frameworks such as Flask and Django, Sanic uses the ASGI-compatible event loop to handle requests concurrently without blocking.

At its core, Sanic leverages Python's asyncio library and the uvloop event loop (a fast drop-in replacement for the default asyncio loop) to achieve performance that rivals Node.js and Go in many benchmarks. If you have ever wanted Flask's simplicity with the throughput of a non-blocking server, Sanic is the framework for you.

Why Sanic Matters

Performance is the headline feature, but Sanic offers several other compelling advantages:

Getting Started

Installation

Sanic requires Python 3.8 or newer. Install it using pip in a virtual environment:

python -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate
pip install sanic

If you want the optional performance boost from uvloop and httptools, install them as well. Sanic will automatically use them if available:

pip install sanic[uvloop] sanic[httptools]

Your First Sanic Application

Create a file named app.py and add the following code:

from sanic import Sanic
from sanic.response import json

app = Sanic("MyFirstApp")

@app.get("/")
async def hello(request):
    return json({"message": "Hello, Sanic!"})

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8000)

Run the application with python app.py and open http://localhost:8000 in your browser or use curl:

curl http://localhost:8000/
# {"message":"Hello, Sanic!"}

Notice that the handler is defined with async def. This is the key difference from Flask: every handler runs inside the event loop, so you can await database calls, HTTP requests, or any other asynchronous operation without blocking the server.

Routing and Path Parameters

Sanic supports dynamic route parameters using the same syntax as Flask. You can capture values from the URL and even enforce types with string, int, float, and path converters.

from sanic import Sanic
from sanic.response import json

app = Sanic("RoutingDemo")

@app.get("/users/<user_id:int>")
async def get_user(request, user_id):
    return json({"user_id": user_id, "type": type(user_id).__name__})

@app.get("/files/<path:path>")
async def get_file(request, path):
    return json({"requested_path": path})

@app.route("/posts/<slug>", methods=["GET", "POST"])
async def handle_post(request, slug):
    if request.method == "POST":
        return json({"action": "create", "slug": slug})
    return json({"action": "fetch", "slug": slug})

The <user_id:int> converter ensures that only numeric values match the route. If a non-integer is supplied, Sanic returns a 404 automatically. The path converter captures the entire remaining URL, including slashes, which is useful for file-path-style routes.

Working with Request Data

Sanic provides convenient access to all parts of an incoming request through the request object. This includes query parameters, JSON bodies, form data, headers, cookies, and files.

from sanic import Sanic
from sanic.response import json

app = Sanic("RequestDemo")

@app.post("/submit")
async def submit(request):
    # JSON body
    data = request.json or {}

    # Query parameters
    page = request.args.get("page", "1")

    # Headers
    user_agent = request.headers.get("user-agent", "unknown")

    # Form data (for application/x-www-form-urlencoded or multipart)
    name = request.form.get("name")

    # Uploaded files
    avatar = request.files.get("avatar")

    return json({
        "json_body": data,
        "page": page,
        "user_agent": user_agent,
        "name": name,
        "has_avatar": avatar is not None,
    })

It is important to guard against missing data. Accessing request.json on a request without a JSON body raises an exception, so always provide a fallback or wrap the access in a try/except block.

Responses

Sanic offers several response helper functions for common content types. Each one returns a HTTPResponse object that the framework sends back to the client.

from sanic import Sanic
from sanic.response import json, text, html, raw, redirect

app = Sanic("ResponseDemo")

@app.get("/json")
async def json_response(request):
    return json({"status": "ok"}, status=200, headers={"X-Custom": "value"})

@app.get("/text")
async def text_response(request):
    return text("Plain text response")

@app.get("/html")
async def html_response(request):
    return html("<h1>Hello HTML</h1>")

@app.get("/raw")
async def raw_response(request):
    return raw(b"\x89PNG binary data here", content_type="image/png")

@app.get("/old-page")
async def old_page(request):
    return redirect("/json")

Streaming Responses

For large responses or server-sent events, you can stream the response body chunk by chunk:

from sanic import Sanic
from sanic.response import streaming

app = Sanic("StreamingDemo")

@app.get("/stream")
async def stream_handler(request):
    async def streaming_fn(response):
        for i in range(10):
            await response.write(f"chunk {i}\n")
            await asyncio.sleep(0.5)
        await response.write("done\n")

    return streaming(streaming_fn, content_type="text/plain")

Blueprints for Modular Applications

As your application grows, putting every route in a single file becomes unmanageable. Sanic's blueprints let you group related routes into modules that you register on the main app.

# users.py
from sanic import Blueprint
from sanic.response import json

users_bp = Blueprint("users", url_prefix="/users")

@users_bp.get("/")
async def list_users(request):
    return json({"users": ["alice", "bob"]})

@users_bp.get("/<user_id>")
async def get_user(request, user_id):
    return json({"user_id": user_id})
# app.py
from sanic import Sanic
from users import users_bp

app = Sanic("ModularApp")
app.blueprint(users_bp)

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8000)

Blueprints support their own middleware, error handlers, and static file registrations, making them a powerful tool for organizing large codebases.

Middleware

Middleware functions run before or after every request. They are perfect for logging, authentication, CORS headers, and request timing. Define them with @app.middleware("request") or @app.middleware("response").

import time
from sanic import Sanic
from sanic.response import json

app = Sanic("MiddlewareDemo")

@app.middleware("request")
async def add_start_time(request):
    request.ctx.start_time = time.time()

@app.middleware("response")
async def add_response_time_header(request, response):
    duration = time.time() - request.ctx.start_time
    response.headers["X-Response-Time"] = f"{duration:.4f}s"

@app.get("/")
async def index(request):
    return json({"message": "check the X-Response-Time header"})

The request.ctx object is a per-request context that you can use to pass data between middleware and handlers safely. Each request gets its own fresh context.

Connecting to a Database

Because Sanic is asynchronous, you should pair it with async database drivers. Below is an example using asyncpg for PostgreSQL. The connection pool is created when the application starts and closed when it stops.

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

app = Sanic("DatabaseDemo")

@app.listener("before_server_start")
async def setup_db(app, loop):
    app.ctx.db = await asyncpg.create_pool(
        host="localhost",
        port=5432,
        user="postgres",
        password="secret",
        database="mydb",
        min_size=5,
        max_size=20,
    )

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

@app.get("/users/<user_id:int>")
async def get_user(request, user_id):
    async with request.app.ctx.db.acquire() as conn:
        row = await conn.fetchrow(
            "SELECT id, name, email FROM users WHERE id = $1", user_id
        )
    if row is None:
        return json({"error": "not found"}, status=404)
    return json(dict(row))

Using a connection pool is critical for performance. Creating a new database connection per request would overwhelm the database under load. The pool reuses connections across concurrent requests efficiently.

Error Handling

Sanic lets you register custom error handlers for specific HTTP status codes or for all unhandled exceptions. This is useful for returning consistent JSON error responses from an API.

from sanic import Sanic
from sanic.response import json
from sanic.exceptions import NotFound

app = Sanic("ErrorDemo")

@app.exception(NotFound)
async def not_found(request, exception):
    return json({"error": "Resource not found", "path": request.path}, status=404)

@app.exception(Exception)
async def server_error(request, exception):
    return json({"error": "Internal server error", "detail": str(exception)}, status=500)

@app.get("/boom")
async def boom(request):
    raise ValueError("Something went wrong")

Best Practices

Deployment

Sanic can run directly in production using its built-in server, but for robust deployments you should use a process manager. Here is an example using app.run with multiple workers:

if __name__ == "__main__":
    app.run(
        host="0.0.0.0",
        port=8000,
        workers=4,
        access_log=True,
        motd=False,
    )

For production, consider running Sanic behind a reverse proxy like Nginx or Caddy. The reverse proxy handles TLS termination, static file serving, and rate limiting, while Sanic focuses on application logic. You can also deploy Sanic using Docker, Gunicorn with the Sanic worker class, or serverless platforms that support ASGI.

# Dockerfile
FROM python:3.11-slim

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .
EXPOSE 8000

CMD ["python", "app.py"]

Conclusion

Sanic bridges the gap between developer experience and raw performance in the Python web ecosystem. Its Flask-like API makes it easy to pick up, while its asynchronous architecture delivers the throughput needed for modern, high-traffic applications. By understanding routing, request and response handling, blueprints, middleware, database integration, and deployment strategies, you now have everything you need to build production-grade async web services with Sanic. Start small with a single endpoint, add blueprints as your application grows, and always keep the event loop unblocked to get the most out of this powerful framework.

— Ad —

Google AdSense will appear here after approval

← Back to all articles