Static Files

Flask serves CSS, JavaScript, images, and other static assets from the static/ folder under the application directory, mounted at the /static/ URL prefix.

Directory Structure

app/
  static/
    css/style.css
    js/main.js
    img/logo.png
  templates/
    index.html
  __init__.py

Referencing in Templates

Always generate static URLs with url_for('static', filename=...) instead of hardcoding paths — templates then keep working when you move the static directory or add a CDN prefix:

<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<script src="{{ url_for('static', filename='js/main.js') }}" defer></script>
<img src="{{ url_for('static', filename='img/logo.png') }}" alt="Logo">

Custom Static Directory and URL Prefix

app = Flask(
    __name__,
    static_folder="assets",       # Disk directory becomes assets/
    static_url_path="/static",    # URL prefix stays /static
)

Blueprints can carry their own static directories for module-level asset isolation:

bp = Blueprint("blog", __name__, static_folder="static", static_url_path="/blog-static")

Cache Control

During development, browser caching often hides your CSS changes. Control the static cache lifetime:

# Development: disable caching
app.config["SEND_FILE_MAX_AGE_DEFAULT"] = 0

# Production: long cache (combined with filename fingerprinting)
app.config["SEND_FILE_MAX_AGE_DEFAULT"] = 31536000

User Uploads and send_from_directory

User-uploaded files should not live in static/ (to avoid uncontrolled access). Serve them through a controlled view:

from flask import send_from_directory

@app.get("/uploads/<path:filename>")
def uploaded_file(filename):
    # send_from_directory performs path safety checks against directory traversal
    return send_from_directory(app.config["UPLOAD_FOLDER"], filename)

Production Best Practices

  • Don't serve heavy static traffic from the Flask process: let Nginx/Caddy serve the static/ directory directly, or use a CDN.
  • Fingerprinting (cache busting): frontend build tools (Vite/Webpack) hash filenames (e.g. app.3f8a2c.js), so a one-year cache is safe.
  • Compression: enable gzip/brotli at the proxy layer rather than in Flask.

Nginx example:

location /static/ {
    alias /srv/myapp/app/static/;
    expires 1y;
    add_header Cache-Control "public, immutable";
}