All articles

2 min read

Three safeguards for a production Flask API

Validation, rate limiting and security headers: the minimum before exposing a public form.

  • Flask
  • Python
  • Security

The problem

A Flask API that works locally is not an API ready for the Internet. A public contact form attracts bots, malformed payloads and injection attempts. Three safeguards cover the essentials.

1. Validate before processing

Browser-side HTML validation is a convenience, not security. The server must revalidate every field, with explicit limits.

from flask import request

MAX_MESSAGE = 5_000

def read_contact_payload() -> dict[str, str]:
    payload = request.get_json(silent=True) or {}
    name = str(payload.get("name", "")).strip()
    email = str(payload.get("email", "")).strip()
    message = str(payload.get("message", "")).strip()

    if not name or len(name) > 120:
        raise ValueError("invalid_name")
    if "@" not in email or len(email) > 254:
        raise ValueError("invalid_email")
    if not message or len(message) > MAX_MESSAGE:
        raise ValueError("invalid_message")

    return {"name": name, "email": email, "message": message}

The honeypot complements validation: a hidden field that only bots fill in. If it is filled, the request is rejected without explanation.

2. Rate limit

Without a limit, a script can send thousands of messages or saturate the SMTP server. Flask-Limiter applies per IP address and per route.

from flask_limiter import Limiter
from flask_limiter.util import get_remote_address

limiter = Limiter(get_remote_address, app=app, default_limits=["120 per hour"])

@app.post("/api/contact")
@limiter.limit("5 per hour")
def contact():
    ...

The limit must be low enough to protect, high enough not to block a legitimate user fixing a typo.

3. Put headers in the right place

Security headers can be added by the proxy, but the API must remain correct even when called directly. A single after_request avoids omissions.

@app.after_request
def security_headers(response):
    response.headers["X-Content-Type-Options"] = "nosniff"
    response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
    response.headers["Cache-Control"] = "no-store"
    return response

What about errors?

A public API must answer in JSON, even when everything goes wrong. A global error handler turns exceptions into consistent responses ({"error": "..."}) with the right HTTP status, without ever leaking the internal trace.

These safeguards do not make an application invulnerable, but they remove the most common mistakes — and they can be tested, which matters as much as the code itself.