featured image

Your Flask App Is a Monolith Until You Learn the Factory: How `create_app`, Blueprints, and Jinja2 Inheritance Turn a Script Into a Scalable Project

A deep dive into the architectural patterns that transform a single-file Flask script into a maintainable, scalable web application — covering the application factory, Blueprint organization, and Jinja2 template inheritance.

Published

Mon Feb 23 2026

Technologies Used

Flask Jinja2
Advanced 41 minutes

Why Most Flask Tutorials Set You Up to Fail — And What a Real Project Looks Like

The Problem

Every introductory Flask tutorial starts the same way:

from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello():
    return 'Hello, World!'

if __name__ == '__main__':
    app.run()

This is fine for a 20-line demo. It becomes a disaster for a real application. Here is why:

  • The app object is a module-level global. Every file that imports it creates a hard dependency on that single module. Circular imports become inevitable the moment your routes need your configuration and your configuration needs your app.
  • There is no configuration switching. You cannot run the same code with a development configuration, a testing configuration, and a production configuration without rewriting environment variables or patching globals.
  • Templates live in a flat directory. Once you have 15 HTML files — some pages, some partials, some layouts — naming collisions and organizational chaos follow.
  • Routes, models, utilities, and templates all live in one file or one flat folder. There is no separation of concerns. Adding a new feature means editing the same monolithic file every time.

STORM DAT is a production Flask application with document analysis, screen recording, file upload/download, and AI transcription — four distinct feature domains. It would be unmaintainable as a single-file script. Instead, it uses three architectural patterns that solve these problems permanently.

The Solution

We will walk through STORM DAT’s project structure from the outermost entry point (run.py) to the innermost template block ({% block body %}), covering:

  1. The Application Factory (create_app) — how to construct Flask apps as function calls, not global objects
  2. Blueprint-Based Route Organization — how to register route groups without circular imports
  3. Jinja2 Template Inheritance — how a single base layout produces four visually consistent but structurally distinct pages

By the end, you will understand how to scaffold a Flask project that can grow from one endpoint to fifty without architectural pain. No external scaffolding tools required — just Python packages, one factory function, and Jinja2’s block system.

Before You Begin: Python Packages, HTTP Basics, and HTML Templating Concepts

Knowledge Base

  • Python packages: the difference between a module (.py file) and a package (directory with __init__.py), how relative imports work (from .utils import security)
  • Flask basics: routes, request, render_template, flash, url_for
  • HTML structure: <head>, <body>, <nav>, and how CSS stylesheets are linked
  • Template concepts: the idea of a “layout” that child pages extend (if you have used Django templates, React layouts, or even WordPress themes, the concept transfers)

Environment

  • Python 3.12
  • Flask 3.1.0 — the web framework
  • Flask-CORS 6.0.1 — cross-origin resource sharing
  • Jinja2 3.1.6 — ships with Flask, provides the template engine
  • Gunicorn 23.0.0 — production WSGI server (deployment)
pip install flask==3.1.0 flask-cors==6.0.1 gunicorn==23.0.0

The Matryoshka Doll: How Nested Layers Build a Complete Application

Think of STORM DAT’s architecture as a Russian nesting doll (matryoshka). Each layer wraps the one inside it, adding structure without breaking what is already there.

flowchart TD
    A["run.py\n(Entry Point)"] -->|"Calls create_app(config)"| B["src/__init__.py\n(Application Factory)"]
    B -->|"Registers"| C["src/routes.py\n(Blueprint)"]
    B -->|"Attaches"| D["src/utils/security_headers.py\n(Middleware)"]
    B -->|"Loads"| E["src/config/config.py\n(Configuration)"]
    B -->|"Initializes"| F["Whisper Model\n(Singleton)"]
    C -->|"Calls render_template"| G["templates/modern_base.html\n(Base Layout)"]
    G -->|"Inherited by"| H["pages/modern_home.html"]
    G -->|"Inherited by"| I["pages/modern_word_upload.html"]
    G -->|"Inherited by"| J["pages/modern_word_result.html"]
    G -->|"Inherited by"| K["pages/modern_record.html"]

    style A fill:#e8f5e9,stroke:#333
    style B fill:#fff3e0,stroke:#333
    style G fill:#e3f2fd,stroke:#333

The outermost doll is run.py — the file you actually execute. It picks a configuration and calls the factory.

The middle doll is create_app() — the factory function that constructs, configures, and returns a Flask instance. It wires together the Blueprint, the middleware, the configuration, and the ML model.

The innermost doll is the template system — a base HTML layout that defines the shell (navigation, flash messages, CSS links), with four child templates that fill in only the content that changes between pages.

Layer by Layer: From Entry Point to Rendered Page

Layer 1 — The Entry Point: run.py

The entry point is deliberately minimal. Its only job is to select a configuration and call the factory:

"""Module to run program"""
import argparse
from src import create_app
from src.config.config import DevelopmentConfig, TestingConfig, ProductionConfig

# Module-level app for Gunicorn: "gunicorn run:app"
app = create_app(config_class=ProductionConfig)

def main(config_class):
    """Create the app using the selected configuration"""
    app = create_app(config_class=config_class)

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description="Run Flask app")
    parser.add_argument('--dev', action='store_true')
    parser.add_argument('--test', action='store_true')
    parser.add_argument('--production', action='store_true')
    args = parser.parse_args()

    configClass = DevelopmentConfig
    if args.production:
        configClass = ProductionConfig
    elif args.test:
        configClass = TestingConfig

    main(configClass)
    app.run()

Two critical details here:

The module-level app = create_app(ProductionConfig) exists for Gunicorn. When the Dockerfile runs gunicorn run:app, Gunicorn imports run.py as a module and looks for an attribute named app. It never executes the if __name__ == '__main__' block — that block only runs during direct python run.py execution. This dual-mode design lets the same file serve both development (python run.py --dev) and production (gunicorn run:app) with zero changes.

The configuration is a dictionary, not a class instance. DevelopmentConfig, TestingConfig, and ProductionConfig are plain Python dicts:

# From src/config/config.py
DevelopmentConfig = {
    "DEBUG": True,
    "Testing": False,
    "VERIFY_SSL": False
}

TestingConfig = {
    "DEBUG": False,
    "TESTING": True,
    "VERIFY_SSL": True
}

ProductionConfig = {
    "DEBUG": False,
    "Testing": False,
    "VERIFY_SSL": True
}

Flask’s app.config.from_object() accepts any object with key-value attributes — including dictionaries. This keeps configuration dead-simple: no class hierarchies, no BaseConfig with inheritance, no metaclasses. A dictionary is the right level of complexity for three environment profiles with three keys each.

🔵 Deep Dive: The separation of the module-level app from the main() function is not a code smell — it is an intentional pattern for WSGI compatibility. WSGI servers (Gunicorn, uWSGI, Waitress) import your entry module and look for a callable named app or application. They do not call main() or run if __name__ == '__main__'. By placing app = create_app(ProductionConfig) at module scope, the factory runs during import, and the fully configured app is available as run.app for any WSGI server.

Layer 2 — The Application Factory: src/__init__.py

This is the architectural heart. The create_app() function constructs a Flask instance and attaches every component the application needs:

def create_app(config_class=None):
    """Create a Flask application instance."""
    app = Flask(__name__)
    CORS(app)

    if config_class:
        app.config.from_object(config_class)

Why a function, not a global? Because a function can be called multiple times with different arguments. This is the core insight of the factory pattern:

  • create_app(DevelopmentConfig) → an app with DEBUG=True and SSL verification disabled
  • create_app(TestingConfig) → an app with TESTING=True for pytest
  • create_app(ProductionConfig) → an app with DEBUG=False and full SSL

In a test suite, you would call create_app(TestingConfig) in a fixture, getting a fresh app instance for every test — no global state leaking between tests.

🔴 Danger: If the app object were a module-level global (as in the “Hello World” pattern), testing would require patching global state between tests. A test that modifies app.config['DEBUG'] would affect every subsequent test. The factory pattern eliminates this entire class of test pollution.

The factory then wires in four subsystems, each on a single line:

    # 1. Register routes
    app.register_blueprint(routes.main)

    # 2. Attach security middleware
    add_security_headers(app)

    # 3. Configure secret key (for flash messages and sessions)
    secret_key = os.environ.get('FLASK_SECRET_KEY')
    if secret_key:
        app.secret_key = secret_key
    else:
        app.secret_key = secrets.token_hex(32)

    # 4. Load the Whisper ML model once at startup
    try:
        import whisper
        app.whisper_model = whisper.load_model("medium")
    except Exception as e:
        app.whisper_model = None

    return app

Each subsystem is a separate module — the factory imports and attaches them, but they know nothing about each other. The Blueprint does not import the security headers module. The security headers module does not import the routes. The configuration does not import the Whisper model. This is loose coupling: each component can be understood, tested, and modified independently.

Layer 3 — The Blueprint: src/routes.py

A Blueprint is Flask’s mechanism for grouping related routes into a portable unit that can be registered on any app. STORM DAT defines a single Blueprint:

from flask import Blueprint, render_template, request, flash, jsonify, current_app

main = Blueprint('main', __name__)

@main.route('/')
def index():
    return render_template('pages/modern_home.html')

@main.route("/storm/word", methods=["GET"])
def upload_word():
    return render_template('pages/modern_word_upload.html')

@main.route("/storm/word/results", methods=["POST"])
def analyze_word():
    # ... analysis logic ...
    return render_template('pages/modern_word_result.html',
                           findings=findings,
                           output_html=html_content,
                           output_filenames=output_filenames)

Three patterns to notice:

Blueprint('main', __name__) instead of @app.route. The main Blueprint is created at module scope without needing a reference to app. This eliminates the circular import problem — routes.py never imports app from __init__.py. Instead, __init__.py imports routes and calls app.register_blueprint(routes.main).

current_app instead of app. Inside a request handler, current_app is a proxy that points to whatever Flask app is currently handling the request. This is how Blueprint routes access the app’s configuration and attached objects (like current_app.whisper_model) without importing the app instance directly.

render_template('pages/modern_home.html'). Templates are organized in a pages/ subdirectory, not dumped flat into templates/. This mirrors the route hierarchy: the URL /storm/word maps to the template pages/modern_word_upload.html.

🔵 Deep Dive: current_app is a Werkzeug LocalProxy backed by a context-local stack. When Flask receives a request, it pushes an application context onto a thread-local stack. current_app always resolves to the top of that stack. This is what makes the factory pattern work with Blueprints — the Blueprint does not know which app it belongs to until a request arrives, at which point current_app resolves to the correct instance. In a test, you push the context manually with app.app_context().

Layer 4 — The Python Package Structure

The directory layout is not accidental. Each subdirectory is a Python package (directory with an __init__.py):

src/
├── __init__.py           ← Application factory (create_app)
├── routes.py             ← Blueprint with all endpoints
├── config/
│   ├── __init__.py       ← Empty (marks as package)
│   └── config.py         ← Configuration dicts + security marking lists
├── utils/
│   ├── __init__.py       ← "Utility modules for security and validation"
│   ├── security.py       ← Filename sanitization, HTML escaping
│   ├── validators.py     ← File extension/size validation
│   └── security_headers.py  ← HTTP security header middleware
├── word_analysis/
│   ├── __init__.py       ← Empty
│   └── word_analysis.py  ← WordAnalyzer class (acronym sweep engine)
├── parse_files/
│   ├── __init__.py       ← Empty
│   └── parse_files.py    ← Parser class (Word, Excel, HTML readers)
├── output_table/
│   ├── __init__.py       ← Empty
│   └── output_table.py   ← WriteExcel class (Excel/HTML output)
├── static/
│   ├── modern-styles.css ← Design system (CSS custom properties)
│   ├── uploads/          ← Temporary uploaded files (cleaned after processing)
│   └── outputs/          ← Generated reports (cleaned after 24 hours)
└── templates/
    ├── modern_base.html  ← Base layout (nav, flash messages, CSS link)
    └── pages/
        ├── modern_home.html         ← Home page (extends base)
        ├── modern_word_upload.html  ← Upload form (extends base)
        ├── modern_word_result.html  ← Results display (extends base)
        └── modern_record.html       ← Screen recorder (extends base)

This structure enforces three rules by convention:

One domain per package. The word_analysis/ package contains only document analysis logic. The parse_files/ package contains only file reading logic. The output_table/ package contains only output generation. A developer looking for “where acronyms are checked” knows immediately to look in word_analysis/, not in routes.py or utils/.

Imports flow downward. routes.py imports from word_analysis, parse_files, output_table, and utils. None of those packages import from routes.py. The factory (__init__.py) imports routes and utils, but routes and utils never import the factory. This one-directional dependency graph prevents circular imports entirely.

Templates mirror routes. The templates/pages/ subdirectory contains one file per page endpoint. The route /storm/word renders pages/modern_word_upload.html. The route /storm/word/results renders pages/modern_word_result.html. When a new feature is added, a new route in routes.py maps to a new template in pages/.

🔴 Danger: Flask resolves template paths relative to the templates/ directory inside the package where Flask(__name__) is called. Since Flask(__name__) is called in src/__init__.py, Flask looks for templates in src/templates/. If you accidentally create a second templates/ directory elsewhere (e.g., at the project root), Flask will not find it — and the error message (“Template not found”) will not tell you why.

Layer 5 — Jinja2 Template Inheritance: One Layout, Four Pages

This is where the architecture becomes visible to the user. STORM DAT’s base template defines the complete HTML shell — everything that appears on every page:

<!-- modern_base.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="{{ url_for('static', filename='modern-styles.css') }}">
    <title>{% block title %}STORM DAT{% endblock title %}</title>
    {% block extra_css %}{% endblock extra_css %}
</head>
<body>
    <!-- Navigation Bar (identical on every page) -->
    <nav class="nav-bar">
        <div class="nav-content">
            <a href="{{ url_for('main.index') }}" class="nav-brand">STORM DAT</a>
            <div class="nav-links">
                <a href="{{ url_for('main.upload_word') }}" class="nav-link">Acronym Sweep</a>
                <a href="{{ url_for('main.record') }}" class="nav-link">Record</a>
                <a href="{{ url_for('main.cleanup_static_files') }}" class="nav-link">Cleanup</a>
            </div>
        </div>
    </nav>

    <!-- Flash Messages (shown conditionally on any page) -->
    {% with messages = get_flashed_messages(with_categories=true) %}
        {% if messages %}
            {% for category, message in messages %}
                <div class="alert alert-{{ category }}">
                    {{ message }}
                </div>
            {% endfor %}
        {% endif %}
    {% endwith %}

    <!-- Page-Specific Content (filled by child templates) -->
    {% block body %}{% endblock body %}

    {% block extra_js %}{% endblock extra_js %}
</body>
</html>

Four blocks define the extension points — the slots that child templates fill:

BlockPurposeDefault
{% block title %}Browser tab title"STORM DAT"
{% block extra_css %}Page-specific CSS (injected into <head>)Empty
{% block body %}Main page contentEmpty
{% block extra_js %}Page-specific JavaScript (injected before </body>)Empty

A child template uses {% extends %} and then overrides only the blocks it needs:

<!-- pages/modern_home.html -->
{% extends 'modern_base.html' %}
{% block title %}STORM Document Analysis Tool - Home{% endblock title %}

{% block body %}
<div class="hero">
    <h1 class="hero-title">STORM Document Analysis Tool</h1>
    <p class="hero-subtitle">
        Automated document analysis with security marking validation...
    </p>
    <div class="button-group">
        <a href="{{ url_for('main.upload_word') }}" class="btn btn-primary btn-lg">
            Acronym Sweep
        </a>
        <a href="{{ url_for('main.record') }}" class="btn btn-secondary btn-lg">
            Record Desktop
        </a>
    </div>
</div>
{% endblock body %}

The home page overrides title and body but does not override extra_css or extra_js — those blocks remain empty, so no extra styles or scripts are loaded. The navigation, flash messages, and main stylesheet are inherited automatically.

Contrast this with the recording page, which overrides all four blocks:

<!-- pages/modern_record.html -->
{% extends 'modern_base.html' %}
{% block title %}Desktop Recorder - STORM DAT{% endblock title %}

{% block extra_css %}
<style>
    /* 300+ lines of page-specific CSS for the recorder UI */
    .record-container { max-width: 1400px; ... }
    #preview { aspect-ratio: 16 / 9; ... }
    @keyframes pulse { ... }
</style>
{% endblock extra_css %}

{% block body %}
<div class="record-container">
    <video id="preview" autoplay muted playsinline></video>
    <!-- Recording controls, transcript panel, etc. -->
</div>
{% endblock body %}

{% block extra_js %}
<script>
    /* 300+ lines of MediaRecorder API, audio processing, transcript display */
    const recordButton = document.getElementById("record-button");
    // ...
</script>
{% endblock extra_js %}

The extra_css and extra_js blocks inject page-specific code into the correct positions in the HTML document (<head> for CSS, before </body> for JS) without modifying the base template. The recorder page gets its custom keyframe animations and JavaScript; the home page gets neither. Both share the same navigation bar and flash message system.

The Glue: url_for and Why Hardcoded URLs Are Technical Debt

Every link and asset reference in STORM DAT’s templates uses url_for() instead of hardcoded paths:

<!-- Links to routes by endpoint name, not URL path -->
<a href="{{ url_for('main.index') }}">Home</a>
<a href="{{ url_for('main.upload_word') }}">Acronym Sweep</a>

<!-- Links to static files by filename -->
<link rel="stylesheet" href="{{ url_for('static', filename='modern-styles.css') }}">

<!-- Links to generated output files -->
<a href="{{ url_for('static', filename='outputs/' + output_filenames.word) }}">
    Download
</a>

url_for('main.upload_word') generates the URL /storm/word by looking up the upload_word function registered on the main Blueprint. If you later rename the URL from /storm/word to /analysis/upload, every template link updates automatically — because the templates reference the function name, not the path.

url_for('static', filename='modern-styles.css') generates the path to src/static/modern-styles.css. In development, this might be /static/modern-styles.css. Behind a CDN, it could be https://cdn.example.com/static/modern-styles.css?v=abc123. The template does not care — url_for resolves the correct path for the current environment.

🔴 Danger: If you hardcode <a href="/storm/word">, every URL change requires a find-and-replace across all templates. If you hardcode <link href="/static/styles.css">, your app breaks the moment it is deployed behind a reverse proxy with a URL prefix (e.g., https://example.com/tools/storm/static/styles.css). url_for handles both cases transparently.

The Flash Message System: Server-Side Feedback Through Template Logic

The base template contains a sophisticated flash message renderer that demonstrates Jinja2’s control flow:

{% with messages = get_flashed_messages(with_categories=true) %}
    {% if messages %}
        <div class="container">
            {% for category, message in messages %}
                <div class="alert alert-{{ category if category in
                     ['error', 'success', 'warning', 'info'] else 'info' }}">
                    <div class="alert-content">
                        <div class="alert-title">
                            {% if category == 'error' %}Error
                            {% elif category == 'success' %}Success
                            {% elif category == 'warning' %}Warning
                            {% else %}Information
                            {% endif %}
                        </div>
                        {{ message }}
                    </div>
                </div>
            {% endfor %}
        </div>
    {% endif %}
{% endwith %}

Three Jinja2 features at work:

  • {% with %} creates a scoped variable. messages exists only within the with block, preventing namespace pollution.
  • get_flashed_messages(with_categories=true) returns a list of (category, message) tuples. Flask’s flash('No file provided', 'error') in routes.py pushes to this queue; the template drains it.
  • Inline conditional {{ category if ... else 'info' }} sanitizes the CSS class name. If a route flashes an unrecognized category (e.g., flash('msg', 'debug')), the template defaults to alert-info instead of generating a broken CSS class alert-debug.

Because this logic lives in the base template, every page inherits flash message support. A route handler can call flash() and return any template — the message will display automatically, regardless of which child template is rendered.

How Flask Resolves Templates, Why __name__ Matters, and the Static File Serving Contract

Template Resolution

When render_template('pages/modern_home.html') is called, Flask searches for the file using a resolution chain:

  1. Check the Blueprint’s own template_folder (if specified when creating the Blueprint — STORM DAT does not override this)
  2. Check the application’s template_folder (default: templates/ relative to the package where Flask(__name__) is called)

Since Flask(__name__) is called in src/__init__.py, and __name__ resolves to src, Flask looks in src/templates/. The path pages/modern_home.html maps to src/templates/pages/modern_home.html.

The __name__ Contract

Flask(__name__) does more than set the app name. Flask uses __name__ to determine three directories:

DirectoryResolutionSTORM DAT Path
template_folder<package>/templates/src/templates/
static_folder<package>/static/src/static/
root_pathDirectory containing the packageProject root

If __init__.py lived at the project root instead of inside src/, Flask would look for templates/ at the root — missing every template. The package boundary (src/) is what anchors all path resolution.

Static File Serving

In development, Flask serves files from src/static/ at the URL path /static/. This is why url_for('static', filename='modern-styles.css') works — Flask registers a built-in route that maps /static/<filename> to the file system. In production behind Gunicorn, this still works, but a reverse proxy (Nginx, Caddy) should serve static files directly for better performance, bypassing the Python application entirely.

🔵 Deep Dive: Flask’s development server serves static files synchronously — every CSS, JS, and image request blocks the single-threaded server from handling other requests. This is negligible in development but catastrophic in production. Gunicorn with 4 workers can serve 4 static file requests concurrently, but each one occupies a worker that could be handling an API call instead. This is why production deployments use a dedicated static file server (Nginx location /static/) or a CDN.

When the Factory Pattern Breaks: Testing Gotchas, Circular Imports, and Template Shadowing

The Double-Import Problem

run.py has app = create_app(ProductionConfig) at module scope. This means the factory runs when the module is imported, not just when it is executed. If a test file writes from run import app, it gets a production-configured app — not a test-configured one. The correct pattern for tests is:

# test_example.py
from src import create_app
from src.config.config import TestingConfig

def test_home_page():
    app = create_app(TestingConfig)
    client = app.test_client()
    response = client.get('/')
    assert response.status_code == 200

Call the factory directly with TestingConfig. Never import app from run.py in tests.

Circular Import Prevention

The factory pattern works because imports flow in one direction:

run.py → src/__init__.py → src/routes.py → src/word_analysis/
                                          → src/parse_files/
                                          → src/output_table/
                                          → src/utils/

If routes.py tried to import create_app from src/__init__.py, a circular import would occur: __init__.py imports routes, which imports __init__.py. The Blueprint pattern breaks this cycle — routes.py creates a Blueprint object instead of importing the app, and the factory registers the Blueprint later.

Template Shadowing

If two Blueprints register templates with the same path (e.g., both have templates/pages/home.html), Flask uses the first one found in its search order. STORM DAT avoids this by using a single Blueprint and a single templates/ directory. In a multi-Blueprint application, each Blueprint should set template_folder to its own directory and prefix template names to prevent collisions.

Flash Messages and Redirects

Flash messages are stored in the session cookie. If a route calls flash('Error', 'error') and then returns a rendered template (not a redirect), the flash message is consumed immediately. But if the user refreshes the page, the flash message will not reappear — it was already consumed. STORM DAT’s analyze_word route renders the template directly after flashing, which means the flash is consumed in the same request-response cycle. This is correct behavior for form submissions that should not be retried on refresh.

🔴 Danger: If you call flash() and then redirect(), the flash message survives the redirect and displays on the next page. But if the redirect target is itself redirected (a double redirect), the flash message survives only the first redirect — it is consumed by the second page. Always test the flash-redirect chain end to end.

You Now Know How to Architect a Flask Application That Can Actually Grow

The core skill from this tutorial is structural separation — the ability to organize a Flask application so that each concern has exactly one home:

  1. The factory pattern decouples construction from configuration. By building apps via create_app(config), you gain the ability to run the same code in development, testing, and production without conditional logic scattered across your modules. Each environment gets its own config dictionary; the application code never changes.

  2. Blueprints decouple routes from the app instance. By defining routes on a Blueprint instead of on app, you eliminate circular imports and make route groups portable. A new feature means a new Blueprint file — not modifications to a growing monolith.

  3. Jinja2 inheritance decouples layout from content. By defining the navigation, flash messages, and CSS link in a base template, every new page inherits a consistent shell. Changes to the navigation bar happen in one file and propagate to every page instantly. Page-specific CSS and JavaScript are scoped to the pages that need them via extra_css and extra_js blocks.

  4. Python packages decouple domains from each other. By organizing word_analysis/, parse_files/, output_table/, and utils/ as separate packages with one-directional imports, each domain can be understood, modified, and tested without touching the others.

These four patterns compose into a project structure that does not fight you when you add the fifth feature, the tenth endpoint, or the third developer. The investment is small — a few __init__.py files, one factory function, one base template — but the payoff compounds with every addition to the codebase.

We respect your privacy.

← View All Tutorials

Related Projects

    Ask me anything!