The Art of Routing in Flask
It's hard to imagine a more critical feature of web frameworks than routing: the humble act of mapping URLs to actions, such as serving pages or data. It isn't often you find somebody sad or miserable enough to expand on such an inglorious feature. As it turns out, I am apparently both and miserable enough to be the kind of person who writes tutorials about routing.
At first glance, it's hard to imagine routing to be an "art." We'll typically reserve a URL path, such as/
or/home
, associate this with a page template, and serve said template to the user, perhaps with added business logic. That perspective works fine for small-scale applications, but meaningful applications (or APIs) aren't static one-to-one mappings. Apps are a medium for data such as user-generated content such as user profiles or author posts, and routes define the way our users will access data which is always changing. To build products larger than ourselves, we need to arm them with the ability to grow in ways we can't foresee, which means defining dynamic routing opportunities that can potentially grow endlessly. This is where we have the chance to be artistic.
Today we're covering the finer details of how to define and build smart routes to accommodate dynamic applications and APIs. If you haveany prior experience with MVC frameworks, you should be able to follow along just fine; almost no previous knowledge of Flask is needed to keep up.
Defining Routes & Views
Every web framework begins with the concept of serving content at a given URL.Routes refer to URL patterns of an app (such asmyapp.com/home _ or _myapp.com/about _). _Views refer to the content to be served at these URLs, whether that be a webpage, an API response, etc.
Flask's "Hello world" example defines aroute listening at the root our app and executes a view function calledhome()
:
fromflaskimportFlaskapp=Flask(__name__)@app.route("/")defhome():return"Hello World!"
@app.route("/")
is a Python decorator that Flask provides to assign URLs in our app to functions easily. It's easy to understand what is happening at first glance: the decorator is telling our@app
that whenever a user visits our app domain (myapp.com) at the given.route()
, execute thehome()
function. If you aren't familiar with Python decorators, they're essentially logic which "wraps" other functions; they always match the syntax of being a line above the function they're modifying.
The names we chose for our view functions hold significance in Flask. Years of web development taught us that URLs tend to change all the time, typically for business or SEO purposes, whereas thenames of pages generally always stay the same. Flask recognizes this by giving us ways to move users around our app by referring to the names of views by essentially saying,"redirect the user to whatever the URL forhome()
happens to be." We'll demonstrate that in a moment, but it's worth recognizing the importance of view names for this reason.
Route HTTP Methods
In addition to accepting the URL of a route as a parameter, the@app.route()
decorator can accept a second argument: a list of accepted HTTP methods. By default, a Flask route accepts all methods on a route (GET, POST, etc.). Providing a list of accepted methods is a good way to build constraints into the route for a REST API endpoint, which only makes sense in specific contexts.
fromflaskimportFlaskapp=Flask(__name__)@app.route("/api/v1/users/",methods=['GET','POST','PUT'])defusers():...
Dynamic Routes & Variable Rules
Static route URLs can only get us so far, as modern-day web applications are rarely straightforward. Let's say we want to create a profile page for every user that creates an account within our app or dynamically generate article URLs based on the publication date. Here is where variable rules come in.
@app.route('/user/<username>')defprofile(username):...@app.route('/<int:year>/<int:month>/<title>')defarticle(year,month,title):...
When defining our route, values within carrot brackets<>
indicate a variable; this enables routes to be dynamically generated. Variables can be type-checked by adding a colon, followed by the data type constraint. Routes can accept the following variable types:
- string: Accepts any text without a slash (the default).
- int: Accepts integers.
- float: Accepts numerical values containing decimal points.
- path: Similar to a string, but accepts slashes.
Unlike static routes, routes created with variable rulesdo accept parameters, with those parameters being the route variables themselves.
Types of View Responses
Now that we're industry-leading experts in defining route URLs, we'll turn our attention to something a bit more involved: route logic. The first thing we should recap is the types of responses a view can result in. The top 3 common ways a route will conclude will be with generating apage template, providing aresponse, orredirecting the user somewhere else (we briefly looked over these inpart 1).
Remember: views always conclude with areturn
statement. Whenever we encounter areturn
statement in a route, we're telling the function to serve whatever we're returning to the user.
Rendering Page Templates
You're probably aware by now that Flask serves webpages via a built-in templating engine calledJinja2 (and if you don't know, now you know). To render a Jinja2 page template, we first must import a built-in Flask function calledrender_template()
. When a view function returnsrender_template()
, it's telling Flask to serve an HTML page to the user which we generate via a Jinja template. Of course, this means we need to be sure our app has atemplates directory:
fromflaskimportFlask,render_templateapp=Flask(__name__,template_folder="templates")
With the above, our app knows that callingrender_template()
in a Flask route will look in our app's/templates folder for the template we pass in. In full, such a route looks like this:
fromflaskimportFlask,render_templateapp=Flask(__name__,template_folder="templates")@app.route("/")defhome():"""Serve homepage template."""returnrender_template("index.html")
render_template()
accepts one positional argument, which is the name of the template found in our templates folder (in this case,index.html ). In addition, we can passvalues to our template as keyword arguments. For example, if we want to set the title and content of our template via our view as opposed to hardcoding the, into our template, we can do so:
fromflaskimportFlask,render_templateapp=Flask(__name__,template_folder="templates")@app.route("/")defhome():"""Serve homepage template."""returnrender_template('index.html',title='Flask-Login Tutorial.',body="You are now logged in!")
For a more in-depth look into how rendering templates work in Flask, check out our piece aboutcreating Jinja templates.
Making a Response Object
If we're building an endpoint intended to respond with information to be used programmatically, serving page templates isn't what we need. Instead, we should look tomake_response()
.
make_response()
allows us to serve up information while also providing a status code (such as 200 or 500), and also allows us to attach headers to the said response. We can even usemake_response()
in tandem withrender_template()
if we want to serve up templates with specific headers! Most of the timemake_response()
is used to provide information in the form of JSON objects:
fromflaskimportFlask,make_responseapp=Flask(__name__)@app.route("/api/v2/test_response")defusers():headers={"Content-Type":"application/json"}returnmake_response('Test worked!',200,headers=headers)
There are three arguments we can pass to make_response(). The first is the body of our response: usually a JSON object or message. Next is a 3-digit integer representing the HTTP response code we provide to the requester. Finally, we can pass response headers if so chose.
Redirecting Users Between Views
The last of our big three route resolutions isredirect()
. Redirect accepts a string, which will be the path to redirect the user to. This can be a relative path, absolute path, or even an external URL:
fromflaskimportFlask,redirectapp=Flask(__name__)@app.route("/login")deflogin():returnredirect('/dashboard.html')
But wait! Remember when we said its best practice to refer to routes by their names andnot by their URL patterns? That's where we useurl_for()
comes in: this built-in function takes the name of a view function asinput, and willoutput the URL route of the provided view. This means that changing route URLs won't result in broken links between pages! Below is thecorrect way to achieve what we did above:
fromflaskimportFlask,redirect,url_forapp=Flask(__name__)@app.route("/login")deflogin():returnredirect(url_for('dashboard')
Building Smarter Views
Building a respectable view requires us to excel at both the soft-skills of working with web frameworks as well as the hard-skills of merely knowing the tools available to us.
The basic "soft-skill" of building a route is conceptually straightforward, but difficult in practice for many newcomers. I'm referring to the basics of MVC: the concept that views should only contain logic resolves the response of a view (not extraneous business logic which should be encapsulated elsewhere). It's a skill that comes from habit and example: a bit out of this post's scope but bears repeating regardless.
Luckily, the "hard-skills" are a bit more straightforward. Here are a couple of tools essential to building elegant routes.
The Request Object
request()
is one of the "global" objects we mentioned earlier. It's available to every route and contains all the context of a request made to the said route. Take a look at what things are attached torequest
which we can access in a route:
- request.method : Contains the method used to access a route, such as GET or POST.
request.method
is absolutely essential for building smart routes: we can use this logic to have one route serve multiple different responses depending on what method was used to call said route. This is how REST APIs provide different results on a GET request versus a POST request (if request.method == 'POST':
can open a block only pertaining to POST requests in our route). - request.args : Contains the query-string parameters of a request that hit our route. If we're building an endpoint that accepts aurl parameter, for example, we can get this from the request as
request.args.get('url’)
. - request.data : Returns the body of an object posted to a route.
- request.form : If a user hits this route as a result of form submission,
request.form
is our way of accessing the information the form posted. For example, to fetch the provided username of a submitted form,request.form['username']
is used. - request.headers : Contains the HTTP response headers of a request.
Here’s an example I took from a popular plugin called Flask-Login: a library that handles user authentication in Flask. This is a complex example of a view that utilizes most of the things we just covered in a single route. I don’t expect you to understand everything that’s happening here, but it’s good to see how powerful and versatile we can make a single route simply by using the properties ofrequest
:
...@app.route('/signup',methods=['GET','POST'])defsignup_page():"""User sign-up page."""signup_form=SignupForm(request.form)# POST: Sign user inifrequest.method=='POST':ifsignup_form.validate():# Get Form Fieldsname=request.form.get('name')email=request.form.get('email')password=request.form.get('password')website=request.form.get('website')existing_user=User.query.filter_by(email=email).first()ifexisting_userisNone:user=User(name=name,email=email,password=generate_password_hash(password,method='sha256'),website=website)db.session.add(user)db.session.commit()login_user(user)returnredirect(url_for('main_bp.dashboard'))flash('A user already exists with that email address.')returnredirect(url_for('auth_bp.signup_page'))# GET: Serve Sign-up pagereturnrender_template('/signup.html',title='Create an Account | Flask-Login Tutorial.',form=SignupForm(),template='signup-page',body="Sign up for a user account.")
The "g" Object
Let's say we want a view to access data thatisn't passed along as part of arequest
object. We already know we can't pass parameters to routes traditionally: this is where we can use Flask'sg
. "G" stands for "global," which isn't a great name since we're restricted by the application context, but that's neither here nor there. The gist is thatg
is an object we can attach values to.
We assign values tog
as such:
fromflaskimportgdefget_test_value():if'test_value'noting:g.test_value='This is a value'returng.test_value
Once set, accessingg.test_value
will give us'This is a value'
, even inside a route.
The preferred way of purging values fromg
is by using.pop()
:
fromflaskimportg@app.teardown_testvaluedefremove_test_value():test_value=g.pop('test_value',None)
It's best not to dwell ong
for too long. It is useful in some situations, but can quickly become confusing and unnecessary: most of what we need can be handled by therequest()
object.
Additional Route-related Logic
We've seen how to map static and dynamic routes to functions/views using the@app.route()
decorator. Flask also empowers us with a number of powerful decorators to supplement the routes we create with.route()
:
@app.before_request()
: Defining a function with the.before_request()
decorator will execute said functionbefore every request is made. Examples of when we might use this could include things like tracking user actions, determining user permissions, or adding a "back button" feature by remembering the last page the user visited before loading the next.@app.endpoint('function_name')
: Setting an "endpoint" is an alternative to@app.route()
that accomplishes the same effect of mapping URLs to logic. This is a fairly advanced use-case which comes into play in larger applications when certain logic must transverse modules known as Blueprints (don't worry if this terminology sounds like nonsense — most people will likely never run into a use case for endpoints).
Error-handling Routes
What happens when a user of our app experiences a fatal error? Flask provides us a decorator callederrorhandler()
which serves as a catch-all route for a given HTTP error. Whenever a user experiences the error code we pass to this decorator, Flask immediately serves a corresponding view:
...@app.errorhandler(404)defnot_found():"""Page not found."""returnmake_response(render_template("404.html"),404)
See how we usedmake_response()
in conjunction withrender_template()
? Not only did we serve up a custom template, but we also provided the correct error message to the browser. We're coming full-circle!
The above example passes404 to the@app.errorhandler()
decorator, but we can passany numericalHTTP error code. We might not know the nuances ofhow a complex app will fail under a load of thousands of users, but unforeseen errors will certainly occur. We can (and should) account for errors before they occur by using the@app.errorhandler()
decorator to ensure our users won't be left in the dark.
...@app.errorhandler(404)defnot_found():"""Page not found."""returnmake_response(render_template("404.html"),404)@app.errorhandler(400)defbad_request():"""Bad request."""returnmake_response(render_template("400.html"),400)@app.errorhandler(500)defserver_error():"""Internal server error."""returnmake_response(render_template("500.html"),500)
Additional Route Decorators via Flask Plugins
Lastly, I can't let you go without a least mentioning some of the awesome decorators provided by Flask's powerful plugins. These are a testament to how powerful Flask routes can become with the addition of custom decorators:
@login_required
(fromFlask-Login): Slap this before any route to immediately protect it from being accessed from logged-out users. If the useris logged in, @login_required lets them in accordingly. If you're interested in user account management in Flask, check ourpost about Flask-Login.@expose
(fromFlask-Admin): Allows views to be created for a custom admin panel.@cache.cached()
(fromFlask-Cache): Cache routes for a set period of time, ie:@cache.cached(timeout=50)
.
On The Route to Greatness
Clearly there's a lot we can do with routes in Flask. Newcomers typically get started with Flask armed only with basic knowledge of routes and responses and manage to get along fine. The beauty of Flask development is the flexibility to build meaningful software immediately with the ability to add functionality when the time is right, as opposed to upfront.
As far as routing goes, we've probably covered more route-related logic than what 95% of Flask apps currently need or utilize in the wild. You're well-equipped to build some cool stuff already, so stop reading and get coding!
Top comments(1)

- LocationMauritius
- WorkIndependent Py Dev at MyOwnAccount
- Joined
++
For further actions, you may consider blocking this person and/orreporting abuse