Organizing and Structuring a Sanic Web Application

A standard Python project typically follows a conventional directory layout to separate concerns. A common root structure looks like this:

project_root
├── docs/          # Project documentation
├── src/           # Source code or package name
├── tests/         # Unit and integration tests
├── README.md      # Project overview
└── requirements.txt  # Dependency list

The focus here is on the internal structure of the src directory (or the package directory) when building a Sanic application.

Minimal Structure for Simple Scripts

For quick prototypes or scripts that do not require maintenance, a single file approach is sufficient. You can place all logic, routes, and configuration inside one file, such as main.py.

Example: Single-file RSS Reader

First, install the necessarry libraries:

pip install feedparser sanic

Then, create a simple endpoint that fetches and returns RSS data as JSON:

from sanic import Sanic
from sanic.response import json
from feedparser import parse

app = Sanic("RSSReader")

@app.get("/")
async def fetch_feed(request):
    rss_url = "http://blog.howie6879.cn/atom.xml"
    parsed = parse(rss_url)
    output = [
        {"heading": item.title_detail["value"], "url": item.link}
        for item in parsed.entries
    ]
    return json(output)

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

Adding Template Rendering

If the requirement evolves to display data in an HTML page rather than JSON, you can integrate Jinja2. Instead of defining the HTML string in the code, it is better to manage templates in a dedicated directory later, but for a minimal example:

from sanic import Sanic
from sanic.response import html, json
from feedparser import parse
from jinja2 import Template

app = Sanic("RSSReader")

html_template = Template("""
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>RSS Feed Viewer</title>
</head>
<body>
    <div>
        {% for item in items %}
        <h3><a href="{{ item.url }}">{{ item.heading }}</a></h3>
        <em>{{ item.date }}</em>
        <hr>
        {% endfor %}
    </div>
</body>
</html>
""")

@app.get("/json")
async def get_json(request):
    parsed = parse("http://blog.howie6879.cn/atom.xml")
    return json([{"heading": e.title_detail["value"], "url": e.link} for e in parsed.entries])

@app.get("/view")
async def get_html(request):
    parsed = parse("http://blog.howie6879.cn/atom.xml")
    records = [
        {"heading": e.title_detail["value"], "url": e.link, "date": e.published}
        for e in parsed.entries
    ]
    return html(html_template.render(items=records))

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

Scalable Project Layout

For larger applications, organize the code into packages. A structured layout improves maintainability:

project_root
├── src/
│   ├── config/       # Configuration classes
│   ├── static/       # CSS, JavaScript, images
│   ├── templates/    # Jinja2 HTML files
│   ├── handlers/     # Route definitions and logic
│   │   ├── __init__.py
│   │   └── rss_routes.py
│   └── server.py     # Entry point
├── tests/
└── requirements.txt

Configuration Module

Define a base configuration and environment-specific overrides.

src/config/base.py

import os

class BaseConfig:
    TIMEZONE = 'Asia/Shanghai'
    BASE_DIR = os.path.dirname(os.path.dirname(__file__))

src/config/development.py

from .base import BaseConfig

class DevelopmentConfig(BaseConfig):
    DEBUG = True

src/config/production.py

from .base import BaseConfig

class ProductionConfig(BaseConfig):
    DEBUG = False

src/config/__init__.py

import os
from .base import BaseConfig

def get_config():
    env = os.getenv('APP_ENV', 'DEV')
    if env == 'PROD':
        from .production import ProductionConfig
        return ProductionConfig()
    else:
        from .development import DevelopmentConfig
        return DevelopmentConfig()

SETTINGS = get_config()

Handlers (Views)

Separate the route logic into a dedicated package.

src/handlers/rss_routes.py

from sanic import Blueprint
from sanic.response import html, json
from feedparser import parse
from jinja2 import Environment, PackageLoader, select_autoescape

import sys

rss_blueprint = Blueprint("rss_bp", url_prefix="/")

async_mode = sys.version_info >= (3, 6)

jinja_env = Environment(
    loader=PackageLoader('handlers', '../templates'),
    autoescape=select_autoescape(['html', 'xml']),
    enable_async=async_mode
)

async def render_template(tpl_name, **ctx):
    tpl = jinja_env.get_template(tpl_name)
    rendered = await tpl.render_async(**ctx)
    return html(rendered)

@rss_blueprint.get("/api/feed")
async def feed_json(request):
    content = parse("http://blog.howie6879.cn/atom.xml")
    results = [
        {"heading": entry.title_detail["value"], "url": entry.link}
        for entry in content.entries
    ]
    return json(results)

@rss_blueprint.get("/feed")
async def feed_html(request):
    content = parse("http://blog.howie6879.cn/atom.xml")
    results = [
        {"heading": e.title_detail["value"], "url": e.link, "date": e.published}
        for e in content.entries
    ]
    return await render_template('feed_display.html', items=results)

src/handlers/__init__.py

from .rss_routes import rss_blueprint

Entry Point

src/server.py

from sanic import Sanic
from src.config import SETTINGS
from src.handlers import rss_blueprint

app = Sanic("StructuredApp")

app.blueprint(rss_blueprint)
app.config.from_object(SETTINGS)
app.static('/static', SETTINGS.BASE_DIR + '/static')

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

Templates

src/templates/feed_display.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Feed Display</title>
    <link rel="stylesheet" href="/static/style.css">
</head>
<body>
    <div class="content">
        {% for item in items %}
        <h3><a href="{{ item.url }}">{{ item.heading }}</a></h3>
        <span>{{ item.date }}</span>
        <hr>
        {% endfor %}
    </div>
</body>
</html>

Static Files

src/static/style.css

.content {
    max-width: 800px;
    margin: 20px auto;
    font-family: sans-serif;
}

Tags: Sanic python web development Project Structure Jinja2

Posted on Tue, 20 Oct 2026 16:15:57 +0000 by henryhund