2025-12-28

Stop Overcomplicating Flask: Your Complete App in Exactly 10 Lines

Python, Flask, Web Dev · Dorian Sotpyrc

A minimal Flask app sketch: three routes, one file, no folder maze
Flask is simple. We just keep forgetting that.

Flask got “simple” — then we made it complicated

The folder-maze pattern

Flask’s reputation is weird: it’s “the simple framework”… until you open someone’s repo and it’s app factories, blueprints, config classes, dependency injection, a /src folder you didn’t ask for, and three layers of wrappers around a single endpoint.

Some of those patterns are legitimate. The problem is timing. People cargo-cult the architecture before the app has earned it.

What beginners (and busy pros) actually need

Most Flask apps in the real world are: a tiny internal tool, a webhook receiver, a quick dashboard, a prototype, or a glue service with 1–3 endpoints. The complexity tax kills momentum. So here’s the reset: start tiny, stay shippable, add structure only when a real constraint shows up.

The 10-line Flask app (HTML, JSON, POST)

The rules (exactly 10 lines)

  • One file. One app.
  • One route returns HTML.
  • One route returns JSON.
  • One route accepts POST JSON safely and returns JSON.
  • Dev-friendly: host/port via env, debug off by default.

Copy-paste snippet

PYTHON
from flask import Flask, request, jsonify
import os

app = Flask(__name__)

@app.get("/")
def home():
    return "<h1>OK</h1><p>/health (JSON), /echo (POST JSON)</p>"

@app.get("/health")
def health():
    return jsonify(status="ok")

@app.post("/echo")
def echo():
    data = request.get_json(silent=True) or {}
    status = 200 if data else 400
    return jsonify(ok=bool(data), data=data), status

if __name__ == "__main__":
    app.run(
        host=os.getenv("HOST", "127.0.0.1"),
        port=int(os.getenv("PORT", "5000")),
        debug=os.getenv("FLASK_DEBUG") == "1",
    )

Why these 10 lines are “real” defaults

Predictable responses

jsonify(...) gives you correct JSON responses (including headers) without playing “string-building API server.” It’s boring. That’s the point.

Safe input handling

request.get_json(silent=True) avoids throwing an exception when the body isn’t JSON (or the Content-Type is wrong). Instead, you get None, and we turn that into {} and a clean 400. That’s a baseline you can put behind a load balancer without surprise tracebacks.

Dev vs prod behavior

Debug is off unless you explicitly set FLASK_DEBUG=1. Host/port come from env because you’ll want that the first time you deploy anywhere.

And yes: the built-in server is for local dev. In production, you run Flask behind a real WSGI server.

Run it locally in 30 seconds

Install + run

BASH
pip install flask
python app.py

Test with curl

BASH
curl http://127.0.0.1:5000/
curl http://127.0.0.1:5000/health

# Valid JSON => 200
curl -s -X POST http://127.0.0.1:5000/echo \
  -H "Content-Type: application/json" \
  -d '{"message":"hi"}'

# Missing/invalid JSON => 400 (still JSON response)
curl -s -X POST http://127.0.0.1:5000/echo -d "not json"

The upgrade path (add only when it hurts)

Logging

Add logging when debugging gets slow or you can’t reproduce an issue. Start with request IDs and structured logs before you invent a monitoring micro-framework. Flask already documents sane logging patterns.

Config

Add config when you have multiple environments (local/staging/prod) or secrets. Don’t bake config into Python modules and pretend it’s “clean.” Use environment variables (12-factor), and layer a config object only when you truly have many knobs.

Templates

Add templates when HTML grows past a couple strings. If your “simple dashboard” turns into a real UI, use Jinja templates. Until then, returning a small HTML snippet is fine—and fast.

Blueprints

Add blueprints when you have multiple logical areas (auth/admin/api), repeated concerns (error handlers, middleware), or your route file becomes a scroll-fest. If you have three routes, blueprints are theater.

Auth

Add auth the moment the app touches real data or leaves localhost. For internal tools, that might be your reverse proxy (SSO) rather than Flask-level auth. Don’t “DIY crypto.” Decide where auth belongs and implement the smallest correct thing.

When to introduce blueprints, factories, and /src

Signals you’ve earned structure

  • You have multiple modules owned by multiple people.
  • You need serious testing boundaries (fixtures, app contexts, dependency overrides).
  • You’re packaging the app or building a real API surface area.
  • Shared concerns (auth, rate limiting, error handling) are repeating everywhere.

A minimal directory split (when you must)

If you’re there, split gently: keep app.py as the entrypoint, move routes into routes.py, move settings into config.py, add templates only when you actually render templates. Structure should be an answer to pain, not a pre-flight ritual.

A 60-second anti-bloat checklist

  • Can this be one file? If yes, keep it one file.
  • Do you have more than ~10 routes? If no, skip blueprints.
  • Do you have more than one environment? If yes, env-based config now.
  • Are you debugging by guessing? If yes, add logging now.
  • Is HTML turning into a mess? If yes, templates now.
  • Is this leaving localhost? If yes, real deployment + auth boundary now.
  • Deploy first, refactor second. Complexity that doesn’t ship is just cosplay.

Related PLEX reading

References & further reading