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.
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:
- The
@app.route('/')decorator maps the root URL path (/) to thehomefunction - The
@app.route('/about')decorator maps the/aboutpath to theaboutfunction - When users visit these URLs, Flask executes the corresponding functions and returns their results as responses
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:
- GET requests typically retrieve data or render a form
- POST requests typically submit data for processing
- Other methods like PUT, DELETE, etc. can be used for RESTful APIs
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:
<username>captures a variable part of the URL and passes it as a parameter to the function<int:post_id>captures and converts a numeric part of the URL to an integer
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
- Avoids hardcoding URLs: If your route patterns change, URLs generated by
url_for()will automatically update - Handles URL generation: Properly escapes parameter values and builds the complete URL
- Supports external URLs: Can generate absolute URLs with the
_external=Trueparameter - Adds query parameters: Any additional keyword arguments become query parameters
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:
request.method: The HTTP method (GET, POST, etc.)request.args: Query string parametersrequest.form: Form data (for POST or PUT requests)request.files: Uploaded filesrequest.json: JSON data (if Content-Type is application/json)request.cookies: Cookies sent with the requestrequest.headers: HTTP headersrequest.remote_addr: IP address of the clientrequest.url: Complete URLrequest.path: URL path
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
/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
- A home page that shows a form to submit a URL for shortening
- A route that processes the form submission and creates a shortened URL
- A route that redirects shortened URLs to their original destinations
- A route that shows statistics for a shortened URL
- Error handling for invalid or expired links
Steps
- Set up a Flask application with appropriate routes
- Create a form for URL submission
- Create a function to generate short codes for URLs
- Store URLs and their short codes (in memory for this exercise)
- Implement redirection for shortened URLs
- 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
- Flask's routing system uses decorators to map URLs to view functions
- Dynamic routes allow for capturing parts of the URL as function parameters
- The
url_for()function should be used to generate URLs instead of hardcoding them - View functions handle requests and return responses of various types
- Blueprints help organize routes into logical groups for larger applications
- Request hooks allow you to execute code before and after request processing
- Error handlers let you customize responses for different error conditions
Additional Resources
- Flask Routing Documentation
- View Decorators
- Flask Blueprints
- Flask Error Handling
- Flask API: URL Route Registrations
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
- Create a Flask application with at least 5 different routes, including dynamic routes
- Implement different HTTP methods (GET, POST) for the same endpoint
- Create a custom URL converter
- Implement custom error handlers for 404 and 500 errors
- Use
url_for()for all links and redirects
Advanced Project
Create a RESTful API for a book management system:
- Use blueprints to organize routes by resource (books, authors, genres)
- Implement CRUD operations for each resource
- Support filtering, sorting, and pagination for list endpoints
- Add proper error handling and status codes
- Implement request hooks for authentication (simulated)
- Support different response formats (JSON, XML) based on Accept header
- Create comprehensive documentation for the API