Flask Fundamentals: Routing, Templates, and Forms
Flask is a lightweight, flexible Python web framework that makes building web applications straightforward and enjoyable. Unlike heavier frameworks that impose strict structures, Flask gives you the freedom to build applications your way while providing essential tools for common tasks.
Three concepts form the foundation of almost every Flask application: routing (handling URLs), templates (generating dynamic HTML), and forms (processing user input). Master these three, and you’ll be able to build functional, interactive web applications.
In this guide, we’ll explore each concept in depth, see how they work together, and build practical examples that you can use immediately in your projects.
Getting Started with Flask
Before diving into the core concepts, let’s set up a basic Flask application:
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello():
return 'Hello, World!'
if __name__ == '__main__':
app.run(debug=True)
Save this as app.py and run it with python app.py. Visit http://localhost:5000 in your browser, and you’ll see “Hello, World!”. This simple example demonstrates Flask’s core pattern: define a route, create a function to handle it, and return a response.
Now let’s explore each fundamental concept in detail.
Part 1: Routing - Handling URLs
Routing is how Flask maps URLs to Python functions. When a user visits a URL, Flask determines which function should handle that request.
Basic Routing
The simplest route uses the @app.route() decorator:
from flask import Flask
app = Flask(__name__)
@app.route('/')
def home():
return 'Welcome to the home page!'
@app.route('/about')
def about():
return 'This is the about page'
@app.route('/contact')
def contact():
return 'Contact us at: [email protected]'
Each route is associated with a URL path and a function. When a user visits that URL, Flask calls the corresponding function and returns its result.
Dynamic Routes with URL Parameters
Routes can include dynamic segments that capture values from the URL:
@app.route('/user/<name>')
def greet_user(name):
return f'Hello, {name}!'
@app.route('/post/<int:post_id>')
def view_post(post_id):
return f'Viewing post number {post_id}'
@app.route('/profile/<username>/posts/<int:post_id>')
def user_post(username, post_id):
return f'{username}\'s post {post_id}'
URL parameters are enclosed in angle brackets. You can specify the data type (like <int:post_id>) to ensure the parameter matches the expected format. Common types include:
<string>: Text (default)<int>: Integer<float>: Decimal number<path>: Text that can include slashes<uuid>: UUID string
HTTP Methods
By default, routes only handle GET requests. To handle other HTTP methods (POST, PUT, DELETE), specify them in the decorator:
@app.route('/submit', methods=['GET', 'POST'])
def submit_form():
if request.method == 'POST':
# Handle form submission
return 'Form received!'
else:
# Show the form
return 'Please submit the form'
@app.route('/api/data', methods=['GET', 'POST', 'PUT', 'DELETE'])
def handle_data():
if request.method == 'GET':
return 'Retrieving data'
elif request.method == 'POST':
return 'Creating data'
elif request.method == 'PUT':
return 'Updating data'
elif request.method == 'DELETE':
return 'Deleting data'
URL Building with url_for()
Instead of hardcoding URLs, use url_for() to generate URLs dynamically:
from flask import Flask, url_for
app = Flask(__name__)
@app.route('/')
def home():
return 'Home'
@app.route('/user/<name>')
def user_profile(name):
return f'Profile for {name}'
# In your code or templates:
# url_for('home') generates '/'
# url_for('user_profile', name='john') generates '/user/john'
This approach is powerful because if you change a route, all generated URLs update automatically.
Organizing Routes
For larger applications, organize routes into separate files using blueprints:
# routes/auth.py
from flask import Blueprint
auth_bp = Blueprint('auth', __name__, url_prefix='/auth')
@auth_bp.route('/login', methods=['GET', 'POST'])
def login():
return 'Login page'
@auth_bp.route('/logout')
def logout():
return 'Logged out'
# app.py
from flask import Flask
from routes.auth import auth_bp
app = Flask(__name__)
app.register_blueprint(auth_bp)
Part 2: Templates - Dynamic HTML Generation
Templates allow you to generate dynamic HTML by combining static HTML with Python logic. Flask uses Jinja2, a powerful templating engine.
Basic Template Usage
Create a templates folder in your project directory. Flask automatically looks for templates there.
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/')
def home():
return render_template('home.html')
@app.route('/user/<name>')
def user_page(name):
return render_template('user.html', username=name)
Create templates/home.html:
<!DOCTYPE html>
<html>
<head>
<title>Home</title>
</head>
<body>
<h1>Welcome to My Website</h1>
<p>This is a simple Flask application.</p>
</body>
</html>
Create templates/user.html:
<!DOCTYPE html>
<html>
<head>
<title>User Profile</title>
</head>
<body>
<h1>Welcome, {{ username }}!</h1>
<p>This is your profile page.</p>
</body>
</html>
Variables passed from your route are accessed in templates using {{ variable_name }}.
Template Variables and Expressions
Jinja2 supports various expressions in templates:
<!-- Variables -->
<p>Hello, {{ name }}!</p>
<!-- Expressions -->
<p>2 + 2 = {{ 2 + 2 }}</p>
<p>{{ name.upper() }}</p>
<p>{{ items|length }}</p>
<!-- Filters -->
<p>{{ text|upper }}</p>
<p>{{ text|lower }}</p>
<p>{{ text|capitalize }}</p>
<p>{{ price|round(2) }}</p>
<p>{{ date|strftime('%Y-%m-%d') }}</p>
Conditionals in Templates
Use if statements to show content conditionally:
{% if user %}
<p>Welcome back, {{ user }}!</p>
{% elif guest %}
<p>Welcome, guest!</p>
{% else %}
<p>Please log in.</p>
{% endif %}
{% if age >= 18 %}
<p>You are an adult.</p>
{% else %}
<p>You are a minor.</p>
{% endif %}
Loops in Templates
Iterate over lists and dictionaries:
<!-- Loop over a list -->
<ul>
{% for item in items %}
<li>{{ item }}</li>
{% endfor %}
</ul>
<!-- Loop over a dictionary -->
<ul>
{% for key, value in data.items() %}
<li>{{ key }}: {{ value }}</li>
{% endfor %}
</ul>
<!-- Loop with index -->
<ol>
{% for item in items %}
<li>{{ loop.index }}: {{ item }}</li>
{% endfor %}
</ol>
<!-- Check if list is empty -->
{% for item in items %}
<p>{{ item }}</p>
{% else %}
<p>No items found.</p>
{% endfor %}
Template Inheritance
Create a base template that other templates extend:
templates/base.html:
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}My Website{% endblock %}</title>
<style>
body { font-family: Arial, sans-serif; }
nav { background-color: #333; color: white; padding: 10px; }
</style>
</head>
<body>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
<a href="/contact">Contact</a>
</nav>
<main>
{% block content %}
Default content goes here.
{% endblock %}
</main>
<footer>
<p>© 2025 My Website. All rights reserved.</p>
</footer>
</body>
</html>
templates/home.html:
{% extends "base.html" %}
{% block title %}Home - My Website{% endblock %}
{% block content %}
<h1>Welcome to My Website</h1>
<p>This is the home page.</p>
{% endblock %}
templates/about.html:
{% extends "base.html" %}
{% block title %}About - My Website{% endblock %}
{% block content %}
<h1>About Us</h1>
<p>Learn more about our website.</p>
{% endblock %}
Template inheritance eliminates code duplication and makes maintaining consistent layouts easy.
Passing Data to Templates
Pass various data types from your route to templates:
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/dashboard')
def dashboard():
user = {
'name': 'John Doe',
'email': '[email protected]',
'role': 'admin'
}
posts = [
{'id': 1, 'title': 'First Post', 'content': 'Content here'},
{'id': 2, 'title': 'Second Post', 'content': 'More content'},
{'id': 3, 'title': 'Third Post', 'content': 'Even more content'}
]
stats = {
'total_users': 150,
'total_posts': 42,
'active_sessions': 8
}
return render_template('dashboard.html',
user=user,
posts=posts,
stats=stats)
templates/dashboard.html:
{% extends "base.html" %}
{% block title %}Dashboard{% endblock %}
{% block content %}
<h1>Dashboard</h1>
<section>
<h2>User Info</h2>
<p>Name: {{ user.name }}</p>
<p>Email: {{ user.email }}</p>
<p>Role: {{ user.role }}</p>
</section>
<section>
<h2>Statistics</h2>
<ul>
<li>Total Users: {{ stats.total_users }}</li>
<li>Total Posts: {{ stats.total_posts }}</li>
<li>Active Sessions: {{ stats.active_sessions }}</li>
</ul>
</section>
<section>
<h2>Recent Posts</h2>
<ul>
{% for post in posts %}
<li>
<strong>{{ post.title }}</strong>
<p>{{ post.content }}</p>
</li>
{% endfor %}
</ul>
</section>
{% endblock %}
Part 3: Forms - Handling User Input
Forms are how users interact with your application. Flask makes it easy to create, display, and process HTML forms.
Basic HTML Forms
Create a simple form in a template:
<form method="POST" action="/submit">
<label for="name">Name:</label>
<input type="text" id="name" name="name" required>
<label for="email">Email:</label>
<input type="email" id="email" name="email" required>
<label for="message">Message:</label>
<textarea id="message" name="message" rows="5"></textarea>
<button type="submit">Submit</button>
</form>
Handling Form Submissions
Process form data in your route:
from flask import Flask, render_template, request
app = Flask(__name__)
@app.route('/contact', methods=['GET', 'POST'])
def contact():
if request.method == 'POST':
# Access form data
name = request.form.get('name')
email = request.form.get('email')
message = request.form.get('message')
# Process the data (save to database, send email, etc.)
print(f"Received message from {name} ({email}): {message}")
return 'Thank you for your message!'
# Show the form for GET requests
return render_template('contact.html')
Form Validation
Validate form data before processing:
from flask import Flask, render_template, request
app = Flask(__name__)
@app.route('/register', methods=['GET', 'POST'])
def register():
errors = {}
if request.method == 'POST':
username = request.form.get('username', '').strip()
email = request.form.get('email', '').strip()
password = request.form.get('password', '')
confirm_password = request.form.get('confirm_password', '')
# Validate username
if not username:
errors['username'] = 'Username is required'
elif len(username) < 3:
errors['username'] = 'Username must be at least 3 characters'
# Validate email
if not email:
errors['email'] = 'Email is required'
elif '@' not in email:
errors['email'] = 'Invalid email format'
# Validate password
if not password:
errors['password'] = 'Password is required'
elif len(password) < 6:
errors['password'] = 'Password must be at least 6 characters'
# Validate password confirmation
if password != confirm_password:
errors['confirm_password'] = 'Passwords do not match'
# If no errors, process registration
if not errors:
# Save user to database
return 'Registration successful!'
return render_template('register.html', errors=errors)
templates/register.html:
{% extends "base.html" %}
{% block title %}Register{% endblock %}
{% block content %}
<h1>Register</h1>
<form method="POST">
<div>
<label for="username">Username:</label>
<input type="text" id="username" name="username" required>
{% if errors.username %}
<span style="color: red;">{{ errors.username }}</span>
{% endif %}
</div>
<div>
<label for="email">Email:</label>
<input type="email" id="email" name="email" required>
{% if errors.email %}
<span style="color: red;">{{ errors.email }}</span>
{% endif %}
</div>
<div>
<label for="password">Password:</label>
<input type="password" id="password" name="password" required>
{% if errors.password %}
<span style="color: red;">{{ errors.password }}</span>
{% endif %}
</div>
<div>
<label for="confirm_password">Confirm Password:</label>
<input type="password" id="confirm_password" name="confirm_password" required>
{% if errors.confirm_password %}
<span style="color: red;">{{ errors.confirm_password }}</span>
{% endif %}
</div>
<button type="submit">Register</button>
</form>
{% endblock %}
Using Flask-WTF for Advanced Form Handling
For more complex applications, use Flask-WTF, which provides CSRF protection and form validation:
from flask import Flask, render_template, request
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField
from wtforms.validators import DataRequired, Email, Length, EqualTo
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key-here'
class RegistrationForm(FlaskForm):
username = StringField('Username',
validators=[DataRequired(), Length(min=3, max=20)])
email = StringField('Email',
validators=[DataRequired(), Email()])
password = PasswordField('Password',
validators=[DataRequired(), Length(min=6)])
confirm_password = PasswordField('Confirm Password',
validators=[DataRequired(),
EqualTo('password')])
submit = SubmitField('Register')
@app.route('/register', methods=['GET', 'POST'])
def register():
form = RegistrationForm()
if form.validate_on_submit():
# Form is valid, process it
username = form.username.data
email = form.email.data
# Save to database
return 'Registration successful!'
return render_template('register.html', form=form)
templates/register.html (with Flask-WTF):
{% extends "base.html" %}
{% block title %}Register{% endblock %}
{% block content %}
<h1>Register</h1>
<form method="POST" novalidate>
{{ form.hidden_tag() }}
<div>
{{ form.username.label }}
{{ form.username(size=32) }}
{% if form.username.errors %}
<ul style="color: red;">
{% for error in form.username.errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
<div>
{{ form.email.label }}
{{ form.email(size=32) }}
{% if form.email.errors %}
<ul style="color: red;">
{% for error in form.email.errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
<div>
{{ form.password.label }}
{{ form.password(size=32) }}
{% if form.password.errors %}
<ul style="color: red;">
{% for error in form.password.errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
<div>
{{ form.confirm_password.label }}
{{ form.confirm_password(size=32) }}
{% if form.confirm_password.errors %}
<ul style="color: red;">
{% for error in form.confirm_password.errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
<div>
{{ form.submit() }}
</div>
</form>
{% endblock %}
Putting It All Together: A Complete Example
Let’s build a simple blog application that demonstrates routing, templates, and forms working together:
from flask import Flask, render_template, request, redirect, url_for
app = Flask(__name__)
# In-memory storage (in real apps, use a database)
posts = [
{'id': 1, 'title': 'First Post', 'content': 'This is my first post'},
{'id': 2, 'title': 'Second Post', 'content': 'Another interesting post'}
]
@app.route('/')
def home():
return render_template('blog_home.html', posts=posts)
@app.route('/post/<int:post_id>')
def view_post(post_id):
post = next((p for p in posts if p['id'] == post_id), None)
if post is None:
return 'Post not found', 404
return render_template('blog_post.html', post=post)
@app.route('/create', methods=['GET', 'POST'])
def create_post():
if request.method == 'POST':
title = request.form.get('title')
content = request.form.get('content')
if title and content:
new_post = {
'id': max(p['id'] for p in posts) + 1,
'title': title,
'content': content
}
posts.append(new_post)
return redirect(url_for('view_post', post_id=new_post['id']))
return render_template('blog_create.html')
if __name__ == '__main__':
app.run(debug=True)
templates/blog_home.html:
{% extends "base.html" %}
{% block title %}Blog Home{% endblock %}
{% block content %}
<h1>My Blog</h1>
<a href="{{ url_for('create_post') }}">Create New Post</a>
<div class="posts">
{% for post in posts %}
<article>
<h2><a href="{{ url_for('view_post', post_id=post.id) }}">{{ post.title }}</a></h2>
<p>{{ post.content[:100] }}...</p>
</article>
{% endfor %}
</div>
{% endblock %}
templates/blog_post.html:
{% extends "base.html" %}
{% block title %}{{ post.title }}{% endblock %}
{% block content %}
<h1>{{ post.title }}</h1>
<p>{{ post.content }}</p>
<a href="{{ url_for('home') }}">Back to Blog</a>
{% endblock %}
templates/blog_create.html:
{% extends "base.html" %}
{% block title %}Create Post{% endblock %}
{% block content %}
<h1>Create New Post</h1>
<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" rows="10" required></textarea>
</div>
<button type="submit">Create Post</button>
<a href="{{ url_for('home') }}">Cancel</a>
</form>
{% endblock %}
Best Practices
1. Organize Your Project Structure
my_flask_app/
โโโ app.py
โโโ config.py
โโโ requirements.txt
โโโ templates/
โ โโโ base.html
โ โโโ home.html
โ โโโ about.html
โ โโโ contact.html
โโโ static/
โ โโโ css/
โ โ โโโ style.css
โ โโโ js/
โ โ โโโ script.js
โ โโโ images/
โโโ routes/
โโโ __init__.py
โโโ main.py
โโโ auth.py
โโโ api.py
2. Use Configuration Files
# config.py
import os
class Config:
SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-key-change-in-production'
DEBUG = False
class DevelopmentConfig(Config):
DEBUG = True
class ProductionConfig(Config):
DEBUG = False
# app.py
from flask import Flask
from config import DevelopmentConfig
app = Flask(__name__)
app.config.from_object(DevelopmentConfig)
3. Use Blueprints for Modularity
# routes/main.py
from flask import Blueprint
main_bp = Blueprint('main', __name__)
@main_bp.route('/')
def home():
return 'Home'
# app.py
from flask import Flask
from routes.main import main_bp
app = Flask(__name__)
app.register_blueprint(main_bp)
4. Validate and Sanitize Input
Always validate user input to prevent security vulnerabilities:
from flask import request
from markupsafe import escape
@app.route('/search', methods=['GET'])
def search():
query = request.args.get('q', '').strip()
# Validate input
if not query:
return 'No search query provided'
if len(query) > 100:
return 'Search query too long'
# Escape for safe HTML rendering
safe_query = escape(query)
# Perform search
return f'Searching for: {safe_query}'
Conclusion
Routing, templates, and forms are the building blocks of Flask applications. By mastering these three concepts, you have the foundation to build dynamic, interactive web applications.
Key Takeaways
- Routing maps URLs to Python functions using decorators
- Templates generate dynamic HTML using Jinja2
- Forms handle user input and enable interaction
- These three concepts work together to create complete web applications
- Use blueprints to organize larger applications
- Always validate and sanitize user input
- Use template inheritance to maintain consistent layouts
Start building Flask applications today. Begin with simple routes and templates, add forms for user interaction, and gradually build more complex applications. The Flask community is welcoming and the documentation is excellentโdon’t hesitate to explore further.
Happy coding!
Comments