Skip to main content
โšก Calmops

Flask Fundamentals: Routing, Templates, and Forms

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>&copy; 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