Use Flask with Claude

Su Suriya Ravichandran Updated 10 Nov 2026
7 min read ·Lesson 67 of 200

Why Flask

Flask is a lightweight Python web framework. It gives you routing, request and response handling, sessions, and templates, and leaves everything else to you. That simplicity makes it a good fit for small chat applications, internal tools, and learning how web chatbots work.

This lesson implements the web chatbot API designed in Lesson 66 with Flask, using the synchronous Anthropic client through the shared chat_service.py module.

Flask conceptUsed for
Routes (@app.get, @app.post)The API endpoints
request.get_json()Reading JSON request bodies
jsonify()Returning JSON responses
sessionA signed cookie holding the user's ID
Response + stream_with_contextStreaming server-sent events
send_from_directoryServing the chat page

Setup

python -m pip install flask anthropic

Flask signs its session cookie with a secret key. Generate one and keep it in an environment variable, never in code (Lesson 8):

python -c "import secrets; print(secrets.token_hex(32))"
export FLASK_SECRET_KEY="paste-the-generated-value-here"
export ANTHROPIC_API_KEY="sk-ant-..."

Project Structure

flask-chatbot/
├── chatbot_core.py      # Lesson 61
├── history_store.py     # Lesson 62
├── sessions.py          # Lesson 63
├── memory.py            # Lesson 64
├── chat_service.py      # Lesson 66
├── flask_app.py         # This lesson
└── static/
    └── chat.html        # Lesson 66

Streaming Responses in Flask

Flask streams a response when you return a Response built from a generator: each yield sends one piece of the body to the browser.

from flask import Response, stream_with_context

return Response(stream_with_context(generator()), mimetype="text/event-stream")
  • mimetype="text/event-stream" tells the browser the body is a stream of server-sent events.
  • stream_with_context() keeps Flask's request context, including session and request, available while the generator runs. Without it, accessing them inside the generator raises an error, because the request context normally ends when the view function returns.

Response headers help streaming work behind proxies:

HeaderPurpose
Cache-Control: no-cachePrevents caches from holding back or storing the stream
X-Accel-Buffering: noTells nginx not to buffer the response, so chunks reach the browser immediately

Practical Example: flask_app.py

"""flask_app.py: the Lesson 66 web chatbot implemented with Flask."""

import os
import uuid

from flask import Flask, Response, jsonify, request, send_from_directory, session, stream_with_context

from chat_service import (check_session, chat_events, create_session,
                          list_sessions, session_messages)
from sessions import SessionNotFound

MAX_MESSAGE_CHARS = 10_000

app = Flask(__name__, static_folder="static")
app.config.update(
    SECRET_KEY=os.environ["FLASK_SECRET_KEY"],
    SESSION_COOKIE_HTTPONLY=True,
    SESSION_COOKIE_SAMESITE="Lax",
)


def current_user() -> str:
    """Return this browser's user ID, creating one on the first visit."""
    if "user_id" not in session:
        session["user_id"] = uuid.uuid4().hex
    return session["user_id"]


def error(code: str, message: str, status: int):
    return jsonify({"error": {"code": code, "message": message}}), status


@app.get("/")
def index():
    return send_from_directory(app.static_folder, "chat.html")


@app.get("/api/sessions")
def get_sessions():
    return jsonify({"sessions": list_sessions(current_user())})


@app.post("/api/sessions")
def post_session():
    return jsonify({"id": create_session(current_user())}), 201


@app.get("/api/sessions/<session_id>/messages")
def get_messages(session_id: str):
    try:
        return jsonify({"messages": session_messages(current_user(), session_id)})
    except SessionNotFound:
        return error("session_not_found", "That conversation was not found.", 404)


@app.post("/api/sessions/<session_id>/chat")
def post_chat(session_id: str):
    data = request.get_json(silent=True) or {}
    text = data.get("message")
    if not isinstance(text, str) or not text.strip():
        return error("empty_message", "Please type a message.", 400)
    if len(text) > MAX_MESSAGE_CHARS:
        return error("message_too_long", "That message is too long. Please shorten it.", 400)

    user_id = current_user()
    try:
        check_session(user_id, session_id)
    except SessionNotFound:
        return error("session_not_found", "That conversation was not found.", 404)

    return Response(
        stream_with_context(chat_events(user_id, session_id, text)),
        mimetype="text/event-stream",
        headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
    )


if __name__ == "__main__":
    app.run(debug=True, threaded=True)

Run it and open http://127.0.0.1:5000:

python flask_app.py

How It Works

  1. Configuration: SECRET_KEY comes from the environment and signs the session cookie, so users cannot alter their user ID. SESSION_COOKIE_HTTPONLY hides the cookie from JavaScript, and SESSION_COOKIE_SAMESITE="Lax" stops browsers from sending it with cross-site POST requests, which protects against CSRF.
  2. current_user() gives each browser a random user ID on its first visit and stores it in the signed session. A real application would use its login system instead (Lesson 66).
  3. error() returns every error in the same JSON shape with an appropriate status code, which the page's api() helper displays (Lessons 69 and 70).
  4. The read endpoints (GET /api/sessions, GET .../messages) call the chat service and return JSON. SessionNotFound becomes a 404 with the same message whether the session is missing or belongs to someone else (Lesson 63).
  5. post_chat() validates the request before streaming:
    • request.get_json(silent=True) returns None instead of raising an error for invalid JSON, and or {} turns that into an empty dictionary;
    • the message must be a non-empty string within the length limit;
    • check_session() verifies ownership. Only then does it return the streaming response. Errors found before streaming get proper status codes.
  6. The stream comes from chat_events() (Lesson 66), which sends text, done, or error events and saves the turn. The view itself contains no chat logic.
  7. threaded=True lets Flask's development server handle several requests at once, so one user's long stream does not block everyone else.

Note: app.run(debug=True) is for development only. In production, run Flask under a WSGI server such as Gunicorn (gunicorn -w 4 --threads 8 flask_app:app), behind a reverse proxy such as nginx, with HTTPS. With multiple threads per worker, each streaming response occupies a thread while it runs, so size the thread count for the number of simultaneous chats you expect.


Testing the API Without a Browser

curl can exercise each endpoint. -c and -b store and send the session cookie, and -N disables output buffering so the stream appears live:

curl -s -c cookies.txt -b cookies.txt -X POST http://127.0.0.1:5000/api/sessions
# {"id":"3f9c2a7e41d84b6f9c1e0a5b7d2f8c61"}

curl -N -s -c cookies.txt -b cookies.txt \
  -H "Content-Type: application/json" \
  -d '{"message": "What is a Python decorator?"}' \
  http://127.0.0.1:5000/api/sessions/3f9c2a7e41d84b6f9c1e0a5b7d2f8c61/chat

Example stream:

data: {"type": "text", "text": "A decorator is a function"}

data: {"type": "text", "text": " that wraps another function..."}

data: {"type": "done", "truncated": false}

Flask Within a Larger Project

SituationApproach
Many endpointsGroup the chat routes in a Flask Blueprint
Existing user accountsReplace current_user() with your login system, for example Flask-Login's current_user.id
Abuse protectionAdd rate limiting per user or IP, for example with Flask-Limiter
Async needsFlask supports async def views, but each request still occupies a worker; for heavily concurrent streaming, consider FastAPI (Lesson 68)
Django projectsThe same chat service works from Django views using StreamingHttpResponse

Common Mistakes

Hardcoding the Secret Key

What happens: anyone who sees the code can forge session cookies.

Fix: load SECRET_KEY from the environment, and keep it secret.

Accessing session or request in a Generator Without stream_with_context

What happens: RuntimeError: Working outside of request context.

Fix: wrap the generator in stream_with_context(), or read everything you need before returning the response, as post_chat() does.

Validating Inside the Stream

What happens: invalid input can only be reported as a stream event with status 200.

Fix: validate and check ownership before returning the streaming Response.

Running the Development Server in Production

What happens: poor performance, no hardening, and debug mode can expose an interactive debugger.

Fix: use a production WSGI server, and never enable debug=True in production.

Buffering Proxies

What happens: the answer arrives all at once instead of streaming.

Fix: send X-Accel-Buffering: no and Cache-Control: no-cache, and check your proxy's buffering settings.


Best Practices

  • Keep views thin: validate, identify the user, call the chat service, and return.
  • Load secrets from the environment, and use secure cookie settings.
  • Validate before streaming, with clear JSON errors.
  • Stream with Response and stream_with_context, with anti-buffering headers.
  • Use a production WSGI server with enough threads for concurrent streams.
  • Add rate limiting and real authentication before going public.

Exercise

  1. Add DELETE /api/sessions/<session_id> using the delete_session() function from Lesson 66's exercise, returning 204 on success.
  2. Move the chat routes into a Blueprint named chat, registered with the prefix /api.
  3. Add a simple per-user limit of 20 chat requests per hour, stored in memory, that returns 429 with the error code rate_limited.
  4. Test the API with curl, including an empty message and a session ID that does not exist.
Show answers for steps 1 and 3

Step 1:

from chat_service import delete_session


@app.delete("/api/sessions/<session_id>")
def remove_session(session_id: str):
    try:
        delete_session(current_user(), session_id)
    except SessionNotFound:
        return error("session_not_found", "That conversation was not found.", 404)
    return "", 204

Step 3:

import time
from collections import defaultdict, deque

recent_requests: dict[str, deque] = defaultdict(deque)


def rate_limited(user_id: str, limit: int = 20, window: int = 3600) -> bool:
    now = time.time()
    timestamps = recent_requests[user_id]
    while timestamps and timestamps[0] < now - window:
        timestamps.popleft()
    if len(timestamps) >= limit:
        return True
    timestamps.append(now)
    return False

At the start of post_chat(), after user_id = current_user():

if rate_limited(user_id):
    return error("rate_limited", "You're sending messages too quickly. Please wait a while.", 429)

A deque holds the request times; old entries are removed from the left. This in-memory version works for a single process. With several workers or servers, store the counts in a shared store such as Redis.

0 Comments

Reviewed before they appear

No comments yet.

Anthropic SDK Using Python
Anthropic SDK Intro Claude API Intro SDK Requirements Install Anthropic SDK Upgrade the SDK Create an API Key Set API Keys Use Environment Variables Import Anthropic Create a Client Send Your First Message Choose a Claude Model Set Max Tokens Read a Response Print Response Text Understand Message Blocks Use System Prompts Send User Messages Set Temperature Handle Stop Reasons Messages API Intro Create Messages Use Message Roles Send Conversation History Continue a Conversation Build a Chat Loop Reset Chat History Use Multiple Turns Inspect Message IDs Read Usage Data Write Clear Prompts Set a System Prompt Add Context to Prompts Use Prompt Templates Add Examples to Prompts Request JSON Output Use XML Tags Control Response Length Improve Prompt Quality Avoid Prompt Injection Read Text Blocks Read Content Blocks Check Stop Reasons Count Input Tokens Count Output Tokens Count Tokens Before Sending Limit Output Tokens Use Stop Sequences Read Request IDs Parse API Responses Streaming Intro Stream Text Output Use Stream Events Use Stream Helpers Get Final Messages Stream Async Responses Handle Stream Errors Build a Live Chat Stream Long Responses Close Streams Safely Build a Chatbot Keep Chat History Manage Chat Sessions Add Chat Memory Build a CLI Chatbot Build a Web Chatbot Use Flask with Claude Use FastAPI with Claude Return JSON from an API Add Chat Error Messages Async SDK Intro Create an Async Client Send Async Messages Use asyncio Run Parallel Requests Stream Async Text Handle Async Errors Set Async Timeouts Close Async Clients Choose Sync or Async Tool Use Intro Define a Tool Create Tool Schemas Send Tools to Claude Read Tool Calls Run Python Tools Return Tool Results Continue After Tool Use Control Tool Choice Handle Tool Errors Use the beta_tool Decorator Create a Tool Runner Add Tool Docstrings Validate Tool Inputs Use Multiple Tools Limit Tool Calls Log Tool Activity Build a Weather Tool Build a Search Tool Build a Calculator Tool Structured Output Intro Request JSON Data Validate JSON Output Use Pydantic Models Handle Invalid JSON Extract Fields from Text Generate Typed Data Use Image URLs Send Base64 Images Read PDFs with Claude Use Output Schemas Build Data Extraction Test Structured Responses Send Image Inputs Upload Files Use File IDs Process Text Files Summarize Documents Extract Table Data Handle File Errors Read Cache Usage Reduce Repeated Input Costs Debug Cache Misses Prompt Caching Intro Enable Prompt Caching Cache System Prompts Cache Long Context Test Cache Performance Use Cache Breakpoints Cache Best Practices Message Batches Intro Create a Message Batch Set Batch IDs Check Batch Status List Message Batches Get Batch Results Process Batch Outputs Handle Failed Batch Items Use Batches for Bulk Tasks Compare Batches and Requests Handle API Errors Handle Rate Limits Retry Failed Requests Set Request Timeouts Handle Network Errors Use Exponential Backoff Build Reliable API Calls Use Extended Thinking Log API Errors Track Request IDs Paginate API Results Set Thinking Budgets Read Thinking Blocks Use Tool Search Use Web Search Tools Use Computer Tools Use Citations Use Token Counting Read Raw Responses Set Custom Headers Use Claude on AWS Bedrock Use Claude on Vertex AI Use Claude on Microsoft Foundry Configure Cloud Credentials Choose a Cloud Provider Compare API Integrations Configure Provider Clients Handle Cloud Errors Use Cloud SDK Extras Deploy Cloud Apps Protect API Keys Use a .env File Ignore Secrets in Git Validate User Input Reduce Prompt Injection Limit Tool Permissions Avoid Unsafe Tool Calls Sanitize Tool Results Set Usage Limits Review Security Logs Test SDK Functions Mock API Responses Test Error Handling Test Streaming Code Measure Token Usage Track API Costs Configure Logging Deploy with Docker Deploy a FastAPI App Monitor Production Apps Build a Claude Chatbot Build a Text Summarizer Build a Code Explainer Build a JSON Extractor Build a PDF Assistant Build a Streaming Chat App Build a Tool-Calling Agent Build a Batch Processor Build a Research Assistant Build a Claude API Dashboard
Ask about this post
AI Ask about this post

Ask questions about Use Flask with Claude and get answers drawn from it.

Signed-in readers only.