Routing and View Functions in Flask

Understanding URL patterns, request handling, and response generation in Flask applications

Introduction to Routing in Flask

Routing is one of the core features of any web framework, and Flask provides a particularly elegant and Pythonic approach to mapping URLs to functions. In this lecture, we'll explore how Flask's routing system works and how to create versatile view functions that handle different types of requests and generate appropriate responses.

"The key to understanding Flask's routing is to embrace the decorator pattern, which allows for a clean, declarative syntax that links URLs to the code that handles them."

What is Routing?

Routing is the process of determining how an application responds to client requests at specific endpoints, which are URLs (or paths) with specific HTTP methods (GET, POST, etc.). In Flask, routes are defined using the @app.route decorator, which maps a URL pattern to a Python function.

flowchart LR A[Client Request] --> B[Web Server] B --> C[WSGI] C --> D[Flask App] D --> E[URL Dispatcher] E --> F{URL Pattern Match?} F -->|Yes| G[View Function] F -->|No| H[404 Not Found] G --> I[Response] H --> I I --> A

Basic Routing Concepts

Simple Routes

The most basic form of routing in Flask involves mapping a fixed URL path to a view function:

from flask import Flask

app = Flask(__name__)

@app.route('/')
def home():
    return 'Hello, World!'

@app.route('/about')
def about():
    return 'About Page'

In this example:

HTTP Methods

By default, routes only respond to GET requests, but you can specify which HTTP methods a route should handle:

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        # Process the login form
        username = request.form['username']
        password = request.form['password']
        return f'Logging in {username}...'
    else:
        # Show the login form
        return render_template('login.html')

This allows you to handle different types of requests at the same URL endpoint:

HTTP Methods in Web Applications

  • GET: Retrieve data (should not modify resources)
  • POST: Submit data to be processed
  • PUT: Update an existing resource
  • DELETE: Remove a resource
  • PATCH: Partially update a resource
  • HEAD: Same as GET but without the response body
  • OPTIONS: Get information about available methods

Dynamic Routes with URL Variables

Flask allows you to define dynamic routes that capture parts of the URL as variables:

Basic URL Variables

@app.route('/user/')
def show_user_profile(username):
    return f'User: {username}'

@app.route('/post/')
def show_post(post_id):
    return f'Post: {post_id}'

In these examples:

Converter Types

Flask supports several built-in converter types for URL variables:

Type Description Example
string Default, accepts any text without slashes (default) /user/<string:name>
int Accepts positive integers /post/<int:post_id>
float Accepts positive floating point values /product/<float:price>
path Like string but accepts slashes /path/<path:subpath>
uuid Accepts UUID strings /user/<uuid:id>
any Matches one of the specified items /api/<any(v1,v2,v3):api_version>

Multiple URL Variables

You can include multiple variables in a single URL pattern:

@app.route('/blog///')
def show_blog_post(year, month, day):
    return f'Blog post from {year}-{month:02d}-{day:02d}'

Real-World Example: RESTful API Routes

Dynamic routes are often used in RESTful APIs to manage resources:

@app.route('/api/users', methods=['GET'])
def get_users():
    users = User.query.all()
    return jsonify([user.to_dict() for user in users])

@app.route('/api/users/', methods=['GET'])
def get_user(user_id):
    user = User.query.get_or_404(user_id)
    return jsonify(user.to_dict())

@app.route('/api/users/', methods=['PUT'])
def update_user(user_id):
    user = User.query.get_or_404(user_id)
    data = request.get_json()
    user.update(data)
    db.session.commit()
    return jsonify(user.to_dict())

@app.route('/api/users/', methods=['DELETE'])
def delete_user(user_id):
    user = User.query.get_or_404(user_id)
    db.session.delete(user)
    db.session.commit()
    return '', 204  # No content response

Custom Converters

Flask allows you to create custom converters for more specialized URL parameter parsing:

Creating a Custom Converter

from werkzeug.routing import BaseConverter

class ListConverter(BaseConverter):
    def __init__(self, url_map, separator='+'):
        super(ListConverter, self).__init__(url_map)
        self.separator = separator
        
    def to_python(self, value):
        return value.split(self.separator)
        
    def to_url(self, values):
        return self.separator.join(super(ListConverter, self).to_url(value)
                                  for value in values)

# Register the custom converter
app.url_map.converters['list'] = ListConverter

@app.route('/tags/')
def show_tags(tags):
    return f'Tags: {", ".join(tags)}'

With this custom converter, a URL like /tags/python+flask+web would be parsed as tags = ['python', 'flask', 'web'].

Another Custom Converter Example: Date Converter

from datetime import datetime
from werkzeug.routing import BaseConverter

class DateConverter(BaseConverter):
    def __init__(self, url_map, format='%Y-%m-%d'):
        super(DateConverter, self).__init__(url_map)
        self.format = format
        
    def to_python(self, value):
        try:
            return datetime.strptime(value, self.format).date()
        except ValueError:
            raise ValueError(f'Invalid date format: {value}')
            
    def to_url(self, value):
        return value.strftime(self.format)

# Register the custom converter
app.url_map.converters['date'] = DateConverter

@app.route('/events/')
def show_events(event_date):
    return f'Events on {event_date.strftime("%B %d, %Y")}'

This allows for URLs like /events/2023-05-15 to be automatically converted to Python date objects.

URL Building with url_for

Instead of hardcoding URLs in your application, Flask provides the url_for() function to generate URLs based on route function names:

from flask import url_for

@app.route('/')
def home():
    # Generate URL for the 'about' route
    about_url = url_for('about')
    return f'Home page. About'

@app.route('/about')
def about():
    # Generate URL for the 'home' route
    home_url = url_for('home')
    return f'About page. Home'

@app.route('/user/')
def user_profile(username):
    return f'User: {username}'

@app.route('/redirect-example')
def redirect_example():
    # Redirect to the user profile page for 'john'
    return redirect(url_for('user_profile', username='john'))

Benefits of url_for

Advanced url_for Usage

# With query parameters
url_for('search', q='flask tutorial', page=2)
# Result: '/search?q=flask+tutorial&page=2'

# With external URL
url_for('home', _external=True)
# Result: 'http://example.com/'

# With anchor
url_for('user_profile', username='john', _anchor='bio')
# Result: '/user/john#bio'

Why Use url_for?

Always use url_for() instead of hardcoding URLs in your templates and redirects. This makes your application more maintainable and helps avoid broken links if you change your URL structure.

Route Registration Options

Endpoint Names

By default, the route's endpoint name is the same as the function name, but you can override it:

@app.route('/contact', endpoint='contact_page')
def contact():
    return 'Contact Page'
    
# Now use the endpoint name
url = url_for('contact_page')  # Instead of url_for('contact')

Subdomain Routing

You can route based on subdomains by specifying the subdomain parameter:

# Configure Flask to handle subdomains
app = Flask(__name__)
app.config['SERVER_NAME'] = 'example.com:5000'  # Needed for subdomain routing

@app.route('/', subdomain='')
def subdomain_home(subdomain):
    return f'Subdomain: {subdomain}'
    
# blog.example.com:5000/ will call subdomain_home('blog')

URL Prefixes

For groups of related routes, you can use blueprints with URL prefixes:

from flask import Blueprint

admin_bp = Blueprint('admin', __name__, url_prefix='/admin')

@admin_bp.route('/')
def admin_home():
    return 'Admin Home'
    
@admin_bp.route('/users')
def admin_users():
    return 'Admin Users'
    
# Register the blueprint
app.register_blueprint(admin_bp)

# Results in:
# /admin/ -> admin_home()
# /admin/users -> admin_users()

Route Decorators with Parameters

The route() decorator accepts several parameters:

@app.route('/login', 
          methods=['GET', 'POST'],  # HTTP methods
          strict_slashes=False,     # Whether /login/ is the same as /login
          redirect_to='/new-login', # Redirect to another route
          defaults={'lang': 'en'},  # Default values for view function
          host='example.com')       # Host to match
def login(lang):
    return f'Login page in {lang}'

View Functions and Responses

View functions are the Python functions that handle requests. They receive the request information and return a response.

Request Object

The request object contains all the information about the current HTTP request:

from flask import request

@app.route('/search')
def search():
    query = request.args.get('q', '')
    page = request.args.get('page', 1, type=int)
    return f'Search for: {query}, Page: {page}'
    
@app.route('/upload', methods=['POST'])
def upload():
    if 'file' not in request.files:
        return 'No file part'
        
    file = request.files['file']
    
    if file.filename == '':
        return 'No selected file'
        
    # Process the file
    return f'File uploaded: {file.filename}'

Common request attributes and methods:

Response Types

View functions can return different types of responses:

from flask import make_response, jsonify, send_file, send_from_directory, abort, render_template

@app.route('/text')
def text_response():
    return 'Plain text response'  # String (converted to response with text/html mimetype)

@app.route('/html')
def html_response():
    return '

HTML Response

' # HTML string @app.route('/template') def template_response(): return render_template('page.html', title='Template Page') # Rendered template @app.route('/json') def json_response(): data = {'name': 'Flask', 'version': '2.0.1'} return jsonify(data) # JSON response @app.route('/file') def file_response(): return send_file('static/report.pdf') # File response @app.route('/error') def error_response(): abort(404) # HTTP error response @app.route('/custom') def custom_response(): response = make_response('Custom response', 201) response.headers['X-Custom-Header'] = 'Custom Value' response.set_cookie('user_id', '12345') return response # Custom response object

Response Status Codes

There are several ways to set the status code of a response:

# Method 1: Return a tuple
@app.route('/created')
def created():
    return 'Resource created', 201

# Method 2: Use make_response
@app.route('/accepted')
def accepted():
    response = make_response('Request accepted')
    response.status_code = 202
    return response

# Method 3: Use status_code attribute (Flask 2.0+)
@app.route('/user')
def user_not_found():
    resp = jsonify({'error': 'User not found'})
    resp.status_code = 404
    return resp

Real-World Example: API Response Handling

Here's an example of a more complete API view function with proper response handling:

from flask import Flask, request, jsonify
from werkzeug.exceptions import BadRequest, NotFound
import uuid

app = Flask(__name__)

# Simulate a database
USERS = {
    '1': {'id': '1', 'name': 'John', 'email': 'john@example.com'},
    '2': {'id': '2', 'name': 'Jane', 'email': 'jane@example.com'}
}

@app.route('/api/users', methods=['GET'])
def get_users():
    """Get all users or filter by query parameters."""
    # Get query parameters
    name_filter = request.args.get('name')
    
    if name_filter:
        # Filter users by name (case-insensitive)
        filtered_users = [
            user for user in USERS.values() 
            if name_filter.lower() in user['name'].lower()
        ]
        return jsonify(filtered_users)
    
    # Return all users
    return jsonify(list(USERS.values()))

@app.route('/api/users/', methods=['GET'])
def get_user(user_id):
    """Get a specific user by ID."""
    user = USERS.get(user_id)
    
    if not user:
        # Return 404 if user not found
        return jsonify({'error': 'User not found'}), 404
    
    return jsonify(user)

@app.route('/api/users', methods=['POST'])
def create_user():
    """Create a new user."""
    # Check for JSON content type
    if not request.is_json:
        return jsonify({'error': 'Content-Type must be application/json'}), 415
    
    data = request.get_json()
    
    # Validate required fields
    if not data or not all(k in data for k in ('name', 'email')):
        return jsonify({
            'error': 'Missing required fields',
            'required': ['name', 'email']
        }), 400
    
    # Create new user with UUID
    user_id = str(uuid.uuid4())
    new_user = {
        'id': user_id,
        'name': data['name'],
        'email': data['email']
    }
    
    # Save user
    USERS[user_id] = new_user
    
    # Return created user with 201 status
    response = jsonify(new_user)
    response.status_code = 201
    response.headers['Location'] = f'/api/users/{user_id}'
    return response

@app.route('/api/users/', methods=['PUT'])
def update_user(user_id):
    """Update an existing user."""
    # Check if user exists
    if user_id not in USERS:
        return jsonify({'error': 'User not found'}), 404
    
    # Check for JSON content type
    if not request.is_json:
        return jsonify({'error': 'Content-Type must be application/json'}), 415
    
    data = request.get_json()
    
    # Validate required fields
    if not data or not all(k in data for k in ('name', 'email')):
        return jsonify({
            'error': 'Missing required fields',
            'required': ['name', 'email']
        }), 400
    
    # Update user
    USERS[user_id] = {
        'id': user_id,
        'name': data['name'],
        'email': data['email']
    }
    
    return jsonify(USERS[user_id])

@app.route('/api/users/', methods=['DELETE'])
def delete_user(user_id):
    """Delete a user."""
    # Check if user exists
    if user_id not in USERS:
        return jsonify({'error': 'User not found'}), 404
    
    # Delete user
    del USERS[user_id]
    
    # Return empty response with 204 No Content
    return '', 204

# Error handlers
@app.errorhandler(404)
def not_found(error):
    return jsonify({'error': 'Resource not found'}), 404

@app.errorhandler(400)
def bad_request(error):
    return jsonify({'error': 'Bad request'}), 400

@app.errorhandler(500)
def server_error(error):
    return jsonify({'error': 'Internal server error'}), 500

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

Route Grouping with Blueprints

Blueprints allow you to organize routes, templates, static files, and other functionality into logical groups that can be registered with an application. They're especially useful for larger applications.

Creating a Blueprint

# auth/routes.py
from flask import Blueprint, render_template, redirect, url_for, flash

# Create a blueprint
auth_bp = Blueprint('auth', __name__, url_prefix='/auth',
                   template_folder='templates', static_folder='static')

@auth_bp.route('/login', methods=['GET', 'POST'])
def login():
    # Logic for login
    return render_template('auth/login.html')

@auth_bp.route('/register', methods=['GET', 'POST'])
def register():
    # Logic for registration
    return render_template('auth/register.html')

@auth_bp.route('/logout')
def logout():
    # Logic for logout
    flash('You have been logged out')
    return redirect(url_for('main.index'))

Registering a Blueprint

# app/__init__.py
from flask import Flask

def create_app():
    app = Flask(__name__)
    
    # Register blueprints
    from app.auth.routes import auth_bp
    from app.main.routes import main_bp
    
    app.register_blueprint(auth_bp)
    app.register_blueprint(main_bp)
    
    return app

URL Building with Blueprints

When using url_for() with blueprints, you need to prefix the view function name with the blueprint name:

# Without blueprint
url_for('login')  # /login

# With blueprint
url_for('auth.login')  # /auth/login
graph TB A[Flask Application] --> B[Blueprints] B --> C[Auth Blueprint
/auth prefix] B --> D[Main Blueprint
/prefix] B --> E[Admin Blueprint
/admin prefix] C --> F[/auth/login] C --> G[/auth/register] C --> H[/auth/logout] D --> I[/] D --> J[/about] D --> K[/contact] E --> L[/admin/dashboard] E --> M[/admin/users]

Request and Response Hooks

Flask provides several hooks that allow you to execute code before or after each request is processed.

Before Request

@app.before_request
def load_user():
    """Execute before each request."""
    if 'user_id' in session:
        g.user = User.query.get(session['user_id'])
    else:
        g.user = None

After Request

@app.after_request
def add_header(response):
    """Execute after each request if no exceptions occurred."""
    response.headers['X-Frame-Options'] = 'DENY'
    response.headers['X-Content-Type-Options'] = 'nosniff'
    return response

Teardown Request

@app.teardown_request
def close_db(exception):
    """Execute after each request, even if an exception occurred."""
    db = g.pop('db', None)
    if db is not None:
        db.close()

Teardown Appcontext

@app.teardown_appcontext
def shutdown_db(exception):
    """Execute when the application context ends."""
    db = g.pop('db', None)
    if db is not None:
        db.close()

Common Uses for Request Hooks

  • before_request: User authentication, database connections, request logging
  • after_request: Response formatting, adding headers, response logging
  • teardown_request: Clean up resources, close connections
  • teardown_appcontext: Clean up application-wide resources

Error Handling and Custom Errors

Flask provides several ways to handle errors and customize error responses.

Error Handlers

@app.errorhandler(404)
def page_not_found(e):
    """Handle 404 errors."""
    return render_template('errors/404.html'), 404

@app.errorhandler(500)
def server_error(e):
    """Handle 500 errors."""
    return render_template('errors/500.html'), 500

Custom Exception Handlers

class InvalidAPIUsage(Exception):
    status_code = 400
    
    def __init__(self, message, status_code=None, payload=None):
        super().__init__()
        self.message = message
        if status_code is not None:
            self.status_code = status_code
        self.payload = payload
        
    def to_dict(self):
        rv = dict(self.payload or ())
        rv['message'] = self.message
        return rv

@app.errorhandler(InvalidAPIUsage)
def handle_invalid_usage(error):
    response = jsonify(error.to_dict())
    response.status_code = error.status_code
    return response

@app.route('/api/item/')
def get_item(id):
    item = Item.query.get(id)
    if item is None:
        raise InvalidAPIUsage(f'Item {id} not found', status_code=404)
    return jsonify(item.to_dict())

Abort Function

from flask import abort

@app.route('/user/')
def show_user(username):
    user = get_user(username)
    if not user:
        abort(404)  # Raises an HTTPException
    if not current_user.can_view(user):
        abort(403)  # Forbidden
    return render_template('user.html', user=user)

Blueprint Error Handlers

Blueprints can have their own error handlers that only apply to routes defined in the blueprint:

admin_bp = Blueprint('admin', __name__, url_prefix='/admin')

@admin_bp.errorhandler(404)
def admin_not_found(e):
    """Handle 404 errors in the admin blueprint."""
    return render_template('admin/errors/404.html'), 404

Practical Exercise

Build a Flask URL Shortener

Create a simple URL shortener application that demonstrates various routing concepts:

Requirements

  1. A home page that shows a form to submit a URL for shortening
  2. A route that processes the form submission and creates a shortened URL
  3. A route that redirects shortened URLs to their original destinations
  4. A route that shows statistics for a shortened URL
  5. Error handling for invalid or expired links

Steps

  1. Set up a Flask application with appropriate routes
  2. Create a form for URL submission
  3. Create a function to generate short codes for URLs
  4. Store URLs and their short codes (in memory for this exercise)
  5. Implement redirection for shortened URLs
  6. Add error handling for various scenarios

Bonus Challenges

  • Add custom URL slugs (user-defined short codes)
  • Implement visit tracking for each shortened URL
  • Add expiration dates for shortened URLs
  • Create an API endpoint for shortening URLs

Summary

Key Takeaways

Additional Resources

mindmap root((Flask Routing)) Basic Routes Static Paths HTTP Methods Endpoint Names Dynamic Routes URL Variables Converters Custom Converters URL Building url_for Function Query Parameters External URLs Responses HTML/Text JSON Templates Files Redirects Organization Blueprints Nested Routes Prefixes Hooks & Errors Request Hooks Error Handlers Custom Exceptions

Next Steps

In our next lecture, we'll explore Jinja2 Templates and Views, learning how to build dynamic and reusable HTML templates for your Flask applications.

Practice Activities

Basic Exercises

  1. Create a Flask application with at least 5 different routes, including dynamic routes
  2. Implement different HTTP methods (GET, POST) for the same endpoint
  3. Create a custom URL converter
  4. Implement custom error handlers for 404 and 500 errors
  5. Use url_for() for all links and redirects

Advanced Project

Create a RESTful API for a book management system: