Site

App: Web Code Review — Form Submission

Tutorial A7.0  •  AI / Learn / Applications

A7.0 What This Teaches

This tutorial builds a stateless web form: the user pastes code into a textarea, submits it, and sees a review on the next page. It covers:

A7.1 Why Stateless

Code review is a one-shot operation. The user submits code, receives a review, and is done. There is no "turn 2" - no need to remember what was said before. Stateless design is simpler: each POST request is completely independent. The server reads the submitted code, calls Claude once, and renders the result. No session object, no database, no history list. Compare this to the Web Chat tutorial (A6), where history accumulates across requests. Picking stateless vs. stateful is an early design decision that shapes the entire application.

A7.2 Complete app.py

# app.py - stateless code review web app.
# GET /  renders the submission form.
# POST / calls Claude and renders the review.

from flask import Flask, render_template, request
import anthropic

app = Flask(__name__)
client = anthropic.Anthropic()

SYSTEM = (
    "You are an expert code reviewer. "
    "Review the submitted code for correctness, style, and potential bugs. "
    "Structure your response as: (1) Summary, (2) Issues Found, (3) Suggestions."
)

@app.route("/", methods=["GET", "POST"])
def index():
    if request.method == "GET":
        return render_template("index.html")

    code     = request.form.get("code", "").strip()
    language = request.form.get("language", "code")

    if not code:
        return render_template("index.html", error="Please paste some code before submitting.")

    prompt = f"Review this {language} code:\n\n```{language}\n{code}\n```"

    response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=1024,
        system=SYSTEM,
        messages=[{"role": "user", "content": prompt}],
    )

    review = response.content[0].text
    return render_template("review.html", review=review, code=code, language=language)

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

A7.3 The Form Template

The form has a language selector, a code textarea, and a submit button. The name attributes on each field match what request.form reads in the route.
<!-- templates/index.html - code submission form -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Code Review</title>
  <style>
    body { font-family: sans-serif; max-width: 700px; margin: 2rem auto; }
    textarea { width: 100%; height: 300px; font-family: monospace; }
    select, button { margin-top: 0.5rem; }
  </style>
</head>
<body>
  <h1>Code Review</h1>
  {% if error %}<p style="color:red">{{ error }}</p>{% endif %}
  <form method="POST">
    <label>Language:
      <select name="language">
        <option value="Python">Python</option>
        <option value="JavaScript">JavaScript</option>
        <option value="C++">C++</option>
        <option value="Rust">Rust</option>
        <option value="C#">C#</option>
      </select>
    </label>
    <br>
    <textarea name="code" placeholder="Paste your code here..."></textarea>
    <br>
    <button type="submit">Review</button>
  </form>
</body>
</html>

A7.4 The Review Template

The review page shows the submitted code in a <pre> block and the review text below it. The Jinja2 | e filter escapes HTML characters in the user-supplied code so it cannot inject markup into the page.
<!-- templates/review.html - displays review results -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Review Result</title>
  <style>
    body { font-family: sans-serif; max-width: 700px; margin: 2rem auto; }
    pre  { background: #f4f4f4; padding: 1rem; overflow-x: auto; }
    .review { white-space: pre-wrap; }
  </style>
</head>
<body>
  <h1>Review: {{ language }}</h1>
  <h2>Submitted Code</h2>
  <pre>{{ code | e }}</pre>
  <h2>Review</h2>
  <div class="review">{{ review | e }}</div>
  <br>
  <a href="/">&larr; Review another file</a>
</body>
</html>
{{ review | e }} escapes any HTML in the API response too, which prevents a malicious model response from injecting script tags.

A7.5 Complete Code

The full project is three files: app.py (shown in A7.2), templates/index.html (shown in A7.3), and templates/review.html (shown in A7.4). They are complete and runnable as shown - no additional code is needed. Install dependencies:
pip install flask anthropic

A7.6 Running and Testing

cd web_review
python app.py
Open http://127.0.0.1:5000. Paste a small Python function, select Python from the dropdown, and click Review. The review page loads with the submitted code above and the review below.
 * Running on http://127.0.0.1:5000
 * Debug mode: on
Try submitting empty input to confirm the error message appears correctly.

A7.7 Exercise

Exercise Add a download link on the review page that lets the user save the review as a plain text file.
  1. Add a route GET /download that reads a text query parameter and returns it as a plain text file download using Flask's make_response.
  2. On review.html, add a link: <a href="/download?text=...">Download Review</a>. URL-encode the review text in the link using Jinja2's urlencode filter or a hidden form with a POST route.
In the route, set the response headers: Content-Disposition: attachment; filename="review.txt" and Content-Type: text/plain.

A7.8 Common Mistakes

Not escaping user code in the HTML template

If you render user-submitted code with {{ code }} instead of {{ code | e }}, a user who pastes <script>alert(1)</script> will see that script execute in the browser. Always use | e (or Jinja2 autoescape) for any user-supplied content rendered in HTML.

Not handling empty textarea submission

If code is an empty string and you send it to Claude, the model returns a puzzled response about having nothing to review. Check if not code before calling the API and return a helpful error message instead.

Adding conversation history to a stateless form

Some developers add a messages list to the stateless review app thinking it improves context. It does not - the user submits one piece of code and receives one review. Adding history adds complexity without benefit here. Use the Chat app (A6) when multi-turn conversation is genuinely needed.

Key Terms

TermMeaning
statelessEach request carries all the data it needs; the server keeps no memory between requests
request.formFlask dict-like object containing HTML form fields submitted via POST
render_templateRenders a Jinja2 HTML template with values passed as keyword arguments
Jinja2 | e filterHTML-escapes a string, converting < > & to safe entities
textareaHTML element for multi-line text input; value is read via request.form["name"]
make_responseFlask function for building an HTTP response with custom headers and body
XSSCross-Site Scripting - injecting malicious script tags via unescaped user input