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
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
pip install flask
python app.py
Test with curl
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.