Flask Fundamentals: Routing, Templates, and Request Handling
Flask’s philosophy of being “micro” doesn’t mean it’s limited—it means it gives you exactly what you need without assumptions about how you’ll use it. I’ve built everything from simple APIs to complex web applications with Flask, and its explicit nature has saved me countless debugging hours. When something breaks, you know exactly where to look because you wrote every piece of the puzzle.
The beauty of Flask lies in its request-response cycle simplicity. Every web interaction boils down to receiving a request, processing it, and sending back a response. Flask makes this cycle transparent, which is why it’s such an excellent learning framework.
Understanding Flask’s Request Routing
Routing in Flask connects URLs to Python functions, but there’s more nuance here than most tutorials cover. The routing system is where you define your application’s API—both for browsers and other services that might consume your endpoints.
from flask import Flask, request, render_template
app = Flask(__name__)
@app.route('/')
def home():
return render_template('home.html')
@app.route('/user/<username>')
def user_profile(username):
return f'Welcome, {username}!'
@app.route('/post/<int:post_id>')
def show_post(post_id):
return f'Displaying post {post_id}'
These route decorators demonstrate Flask’s URL variable system. The <username>
captures any string, while <int:post_id>
ensures you receive an integer or returns a 404. This type conversion happens automatically, preventing the common bug of trying to perform integer operations on string data.
What many developers miss is that routes can handle multiple HTTP methods and respond differently to each. This becomes crucial when building forms or APIs:
@app.route('/contact', methods=['GET', 'POST'])
def contact():
if request.method == 'POST':
name = request.form['name']
email = request.form['email']
message = request.form['message']
# Process the form data
return render_template('contact_success.html', name=name)
return render_template('contact_form.html')
This pattern—handling both GET and POST in the same function—keeps related logic together. The GET request shows the form, the POST request processes it. I’ve found this approach more maintainable than separating them into different functions, especially for simple forms.
Template System and Dynamic Content
Flask uses Jinja2 for templating, which brings Python-like syntax to your HTML. The template system is where your application’s data meets its presentation, and understanding this boundary is crucial for maintainable applications.
<!-- templates/base.html -->
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}My App{% endblock %}</title>
</head>
<body>
<nav>
<a href="{{ url_for('home') }}">Home</a>
<a href="{{ url_for('contact') }}">Contact</a>
</nav>
<main>
{% block content %}{% endblock %}
</main>
</body>
</html>
Template inheritance through blocks creates a consistent structure across your application. The url_for
function generates URLs based on your route function names, which means changing a route’s URL pattern won’t break your navigation links.
Here’s how you extend this base template:
<!-- templates/home.html -->
{% extends "base.html" %}
{% block title %}Welcome - My App{% endblock %}
{% block content %}
<h1>Welcome to My Blog</h1>
{% for post in posts %}
<article>
<h2>{{ post.title }}</h2>
<p>{{ post.content[:100] }}...</p>
<a href="{{ url_for('show_post', post_id=post.id) }}">Read more</a>
</article>
{% endfor %}
{% endblock %}
The template receives data from your view function and can iterate, filter, and manipulate it using Jinja2’s syntax. This separation keeps your Python code focused on data processing while templates handle presentation.
Handling Forms and User Input
Form handling is where web applications become interactive, and Flask’s approach is refreshingly straightforward. The key insight is that forms are just another way of sending data to your server—they’re not fundamentally different from API requests.
from flask import Flask, request, render_template, redirect, url_for, flash
@app.route('/blog/new', methods=['GET', 'POST'])
def create_post():
if request.method == 'POST':
title = request.form.get('title')
content = request.form.get('content')
if not title or not content:
flash('Both title and content are required!')
return render_template('create_post.html')
# Save the post (we'll add database integration later)
posts.append({
'id': len(posts) + 1,
'title': title,
'content': content
})
flash('Post created successfully!')
return redirect(url_for('home'))
return render_template('create_post.html')
This example shows several important patterns. First, we validate input and provide feedback using Flask’s flash messaging system. Second, we follow the POST-redirect-GET pattern to prevent duplicate submissions when users refresh the page.
The corresponding template demonstrates how to display flash messages and handle form errors:
<!-- templates/create_post.html -->
{% extends "base.html" %}
{% block content %}
<h1>Create New Post</h1>
{% with messages = get_flashed_messages() %}
{% if messages %}
{% for message in messages %}
<div class="alert">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="POST">
<div>
<label for="title">Title:</label>
<input type="text" id="title" name="title" required>
</div>
<div>
<label for="content">Content:</label>
<textarea id="content" name="content" required></textarea>
</div>
<button type="submit">Create Post</button>
</form>
{% endblock %}
Request Context and Application Structure
Flask’s request context is one of its most elegant features, though it can seem magical at first. The request
object is available in any function during request processing, even if you don’t pass it as a parameter. This works through Flask’s application context system.
from flask import Flask, request, g
import sqlite3
app = Flask(__name__)
app.secret_key = 'your-secret-key-here'
@app.before_request
def load_user():
# This runs before every request
user_id = request.cookies.get('user_id')
if user_id:
g.user = get_user_by_id(user_id)
else:
g.user = None
@app.route('/dashboard')
def dashboard():
if not g.user:
return redirect(url_for('login'))
return render_template('dashboard.html', user=g.user)
The g
object provides a way to store data during request processing. Combined with before_request
hooks, this creates a clean way to handle cross-cutting concerns like authentication without cluttering your view functions.
Error Handling and Debugging
Proper error handling separates professional applications from hobby projects. Flask provides several mechanisms for graceful error handling that improve user experience and make debugging easier.
@app.errorhandler(404)
def not_found(error):
return render_template('404.html'), 404
@app.errorhandler(500)
def internal_error(error):
return render_template('500.html'), 500
@app.route('/api/posts/<int:post_id>')
def api_get_post(post_id):
try:
post = get_post_by_id(post_id)
if not post:
return {'error': 'Post not found'}, 404
return {
'id': post['id'],
'title': post['title'],
'content': post['content']
}
except Exception as e:
app.logger.error(f'Error fetching post {post_id}: {e}')
return {'error': 'Internal server error'}, 500
Custom error handlers ensure users see helpful pages instead of default browser error messages. For API endpoints, returning JSON error responses maintains consistency with successful responses.
Looking Ahead
In our next part, we’ll integrate a database using SQLAlchemy, transforming our simple blog from storing posts in memory to persisting data properly. You’ll learn about database models, relationships, and migrations—the foundation of any serious web application.
We’ll also explore Flask’s blueprint system for organizing larger applications and dive into user authentication and session management. These concepts build directly on the routing and request handling patterns we’ve established here, showing how Flask’s simplicity scales to complex applications.