×
By the end of this chapter, you should be able to:
Now that we have a solid understanding of the basics of Flasks, it's time to build some more powerful web applications. Before we do so, let's quickly review some key concepts relating to the internet.
HTTP - The protocol that clients and servers use to communicate.
Idempotent - An operation is idempotent if calling it multiple times yields the same result. For example, GET
requests should be idempotent.
CRUD - Shorthand for Create, Read, Update, and Delete. We often talk about implementing CRUD on a resource when building web applications.
Resource - A noun
that we operate on. An application can have many resources, but each resource will have its own set of routes and CRUD operations. For example, users
and tweets
are two examples of resources for Twitter.
GET
- for retrieving information or sending information via the query stringPOST
- for sending information via the body of the requestPUT
- for updating an entire resourcePATCH
- for updating a part of a resourceDELETE
- for removing a resourceGET
is usually fasterPOST
is always more secure is information is not transmitted in the query string (the URL bar in the browser can be seen by anyone)GET
is idempotent; POST is not.As applications grow larger and more developers work on them, structuring and naming things like routes becomes something that needs to be standardized. While REST is much more than route standardization (it is a standard for building web applications), one of the ideas is centering applications around resources and naming the routes for those resources appropriately (we call that RESTful routing).
Let's imagine that we have a web app with a resource of students
(resources are ALWAYS in the plural). Here are what the HTTP Verb, RESTful routes and action look like.
GET '/students'
-> render a page called 'index.html'
(typically with information on all students)
GET '/students/new'
-> render a page called 'new.html'
(typically with a form to create a new student)
GET '/students/:id'
-> render a page called 'show.html'
(typically with information on the student with the given id)
GET '/students/:id/edit'
-> render a page called 'edit.html'
(typically with a form to edit the student with the given id)
POST '/students'
-> create a new resource, then redirect.
PATCH '/students/:id'
-> find a resource by the id, update it, then redirect.
DELETE '/students/:id'
-> find a resource by the id, remove it, then redirect.
The filenames index
, new
, show
and edit
are NOT required, but are convention when working with RESTful routing and are highly encouraged (especially by larger frameworks like Ruby on Rails).
Let's build our first CRUD app with Flask! To get started we first need a resource: let's use toys. Since we will be creating toys, we'll start by making a file called toy.py
which will store a simple toy class. In order to make sure that we can uniquely identify each toy, we will add a property called id
that increments by one anytime a toy is created
class Toy(): count = 1 def __init__(self,name): self.name = name self.id = Toy.count Toy.count += 1
Now let's create an app.py
file to start our server with some sample data. When the user visits the route /toys
, let's start by creating an index route where we will return a template that shows all of our toys.
from flask import Flask, render_template from toy import Toy app = Flask(__name__) duplo = Toy(name='duplo') lego = Toy(name='lego') knex = Toy(name='knex') toys = [duplo,lego,knex] @app.route('/toys') def index(): return render_template('index.html', toys=toys)
In order for that first route to work, we now need an index.html
file! First we will need a templates
folder; inside let's create a base.html
and an index.html
. Let's start with the base.html
:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>CRUD Time</title> </head> <body> {% block content %} {% endblock %} </body> </html>
Now let's add to our index.html
and inherit from base.html
. Inside of this template, let's iterate over our list of toys that are being sent to the template from Flask. Try this on your own first!
{% extends 'base.html' %} {% block content %} <ul> {% for toy in toys %} <li>{{ toy.name }}</li> {% endfor %} </ul> {% endblock %}
Now let's start the server using flask run
and head over to localhost:5000/toys
. We should see all of our toys here!
Now that we can show all of our toys, lets create a route to render a page with a form for creating a new toy. This route is not responsible for actually creating the toy, that's for later. Let's call this route new
.
from flask import Flask, render_template from toy import Toy app = Flask(__name__) duplo = Toy(name='duplo') lego = Toy(name='lego') knex = Toy(name='knex') toys = [duplo,lego,knex] @app.route('/toys') def index(): return render_template('index.html', toys=toys) @app.route('/toys/new') def new(): return render_template('new.html')
Now let's create our new.html
file with a form. This form should have one input for the name of the new toy we are making. In order to send data to our server, we have to add a name
attribute in our form. This is how we will access the value of whatever the user types in for the name of the toy. When the form is submitted, we will reuse our index function but with a different HTTP verb: this time, we'll use a POST request.
{% extends 'base.html' %} {% block content %} <form action="{{url_for('index')}}" method="POST"> <input type="text" name="name"> <input type="submit" value="Add a toy"> </form> {% endblock %}
So we have a form, but if you try to create a new toy with this form, you should see a Method Not Allowed error in the browser. This is because we haven't set up our /toys
route to accept POST requests! And even if it could, we haven't told it how to handle such requests by updating the list of toys. Let's make those modifications next:
Before we discuss what the modified route will look like, we should think about what we want to happen when we submit the form. Once we have finished creating a Toy, it would be a bit silly to render another HTML page telling us that we just created a toy. Instead, it would make more sense to go back to the index
page and see an updated list of all the toys. So, how can we make another request to send us the index
page? To do that we have to introduce a concept called "redirecting."
A redirect is actually two separate requests:
route
location
headerTo do this with Flask, we need to import redirect
. We will also import url_for
so that we do not have to "hard code" our routes, as well as request
which we will use to collect data from a form.
from flask import Flask, render_template, redirect, url_for, request from toy import Toy app = Flask(__name__) duplo = Toy(name='duplo') lego = Toy(name='lego') knex = Toy(name='knex') toys = [duplo,lego,knex] @app.route('/toys', methods=["GET", "POST"]) def index(): if request.method == "POST": # gather the value of an input with a name attribute of "name" toys.append(Toy(request.form['name'])) # respond with a redirect to the route which has a function called "index" (in this case that is '/toys') return redirect(url_for('index')) # if the method is GET, just return index.html return render_template('index.html', toys=toys) @app.route('/toys/new') def new(): return render_template('new.html')
Notice that we had to specify in our @app.route
for toys that our application should accept both GET and POST. Then, inside of index
, we can handle each case separately.
Try it out - you should now be able to add new toys inside of your application!
Now that we are able to create new toys, let's make a route to show some additional information about the toys. In order for this to work, we are going to need a way to find individual toys by their id. Let's make a route that includes a dynamic parameter called id
which is an integer. Let's call the function that this route triggers show
.
from flask import Flask, render_template, redirect, url_for, request from toy import Toy app = Flask(__name__) duplo = Toy(name='duplo') lego = Toy(name='lego') knex = Toy(name='knex') toys = [duplo,lego,knex] @app.route('/toys', methods=["GET", "POST"]) def index(): if request.method == "POST": toys.append(Toy(request.form['name'])) return redirect(url_for('index')) return render_template('index.html', toys=toys) @app.route('/toys/new') def new(): return render_template('new.html') @app.route('/toys/<int:id>') def show(id): # find a toy based on its id for toy in toys: if toy.id == id: found_toy = toy # Refactor the code above using a list comprehension! return render_template('show.html', toy=found_toy)
Now let's create a simple page that shows more information about the toy. Let's also add a link for us to edit a toy. We will call this function edit
, and since we need to specify which toy to edit, we will pass an id
of a toy to this url
.
{% extends 'base.html' %} {% block content %} <h1>Let's see some more information about this toy!</h1> <p> The name of the toy is {{toy.name}} - it's a pretty great one. </p> <a href="{{url_for('index')}}">See all the toys</a> <br> <a href="{{url_for('edit', id=toy.id)}}">Edit this toy</a> {% endblock %}
Of course, with that second link present, we'll get an error on this page, because we don't have a route for editing. Let's work on that next.
In order to edit a toy we need to first make sure we have a route that renders a form for editing. Before we can edit a toy, though, we first need to render a page with a form to edit the toy.
from flask import Flask, render_template, redirect, url_for, request from toy import Toy app = Flask(__name__) duplo = Toy(name='duplo') lego = Toy(name='lego') knex = Toy(name='knex') toys = [duplo,lego,knex] @app.route('/toys', methods=["GET", "POST"]) def index(): if request.method == "POST": toys.append(Toy(request.form['name'])) return redirect(url_for('index')) return render_template('index.html', toys=toys) @app.route('/toys/new') def new(): return render_template('new.html') @app.route('/toys/<int:id>') def show(id): # Refactored using a list comprehension! found_toy = [toy for toy in toys if toy.id == id][0] # Refactor the code above to use a generator so that we do not need to do [0]! return render_template('show.html', toy=found_toy) @app.route('/toys/<int:id>/edit') def edit(id): # Refactored using a list comprehension! found_toy = [toy for toy in toys if toy.id == id][0] # Refactor the code above to use a generator so that we do not need to do [0]! return render_template('edit.html', toy=found_toy)
Now let's create a page called edit.html
for us to edit a toy. When this form is submitted we will go back to the route called show, but with a method of "PATCH". We also would like our form to be pre-filled with the values we have for the toy, so let's use the value
attribute on our input for that.
{% extends 'base.html' %} {% block content %} <form action="{{url_for('show', id=toy.id)}}?_method=PATCH" method="POST"> <input type="text" name="name" value="{{toy.name}}"> <input type="submit" value="Add a toy"> </form> {% endblock %}
If you look at the form tag, you may notice that something funky is going on. Why are we setting the method to be PATCH in the action, but POST in the method? Read on to find out!
Unfortunately, forms can only send GET and POST requests. In order to override the default methods, we need to use flask_modus
to override the HTTP verb in headers and query string.
pip install flask-modus
Let's now import what we need. In order to be able to override the default form methods, we need to pass our application into the Modus
function:
from flask import Flask, render_template, redirect, url_for, request from flask_modus import Modus from toy import Toy app = Flask(__name__) modus = Modus(app) # ...
{% extends 'base.html' %} {% block content %} <form action="{{url_for('show', id=toy.id)}}?_method=PATCH" method="POST"> <input type="text" name="name" value="{{toy.name}}"> <input type="submit" value="Add a toy"> </form> {% endblock %}
At this point our HTML is in pretty good shape, but we need to tell our server how to handle PATCH requests. We'll do this by letting /toys/<int:id>
accept both GET and PATCH:
from flask import Flask, render_template, redirect, url_for, request from flask_modus import Modus from toy import Toy app = Flask(__name__) modus = Modus(app) duplo = Toy(name='duplo') lego = Toy(name='lego') knex = Toy(name='knex') toys = [duplo,lego,knex] @app.route('/toys', methods=["GET", "POST"]) def index(): if request.method == "POST": toys.append(Toy(request.form['name'])) return redirect(url_for('index')) return render_template('index.html', toys=toys) @app.route('/toys/new') def new(): return render_template('new.html') @app.route('/toys/<int:id>', methods=["GET", "PATCH"]) def show(id): # Refactored using a generator so that we do not need to do [0]! found_toy = next(toy for toy in toys if toy.id == id) # if we are updating a toy... if request.method == b"PATCH": found_toy.name = request.form['name'] return redirect(url_for('index')) # if we are showing information about a toy return render_template('show.html', toy=found_toy) @app.route('/toys/<int:id>/edit') def edit(id): # Refactored using a generator so that we do not need to do [0]! found_toy = next(toy for toy in toys if toy.id == id) return render_template('edit.html', toy=found_toy)
Note that when you're using flask-modus
and examining the request.method
, you'll get back a bytes literal, not a string. That's why you need to compare to b"PATCH"
, not "PATCH"
. If you want to learn more about the distinction between bytes and strings in Python 3, check out this blog post.
By now you should be able to perform CRU completely within your browser. Next, let's put the D into CRUD by building out the delete functionality.
In our edit.html
we are going to add another form to delete a toy. This is done with a form since we need to make a DELETE request.
{% extends 'base.html' %} {% block content %} <form action="{{url_for('show', id=toy.id)}}?_method=PATCH" method="POST"> <input type="text" name="name" value="{{toy.name}}"> <input type="submit" value="Add a toy"> </form> <form action="{{url_for('show', id=toy.id)}}?_method=DELETE" method="POST"> <input type="submit" value="Delete a toy"> </form> {% endblock %}
Back in our app.py
let's add the necessary code to find a toy, remove it, and then redirect back to the index
route. We can add all of this functionality to our existing show
function:
@app.route('/toys/<int:id>', methods=["GET", "PATCH", "DELETE"]) def show(id): # Refactored using a generator so that we do not need to do [0]! found_toy = next(toy for toy in toys if toy.id == id) # if we are updating a toy... if request.method == b"PATCH": found_toy.name = request.form['name'] return redirect(url_for('index')) if request.method == b"DELETE": toys.remove(found_toy) return redirect(url_for('index')) # if we are showing information about a toy return render_template('show.html', toy=found_toy)
With that, you should have a working CRUD app! Congratulations.
Our toy app is completely functional now, but unfortunately it doesn't look too hot. Let's fix this by adding some CSS to our application!
To add CSS, JavaScript, or any other static assets to a Flask app, you'll first need to create a directory called static
at the root of your project. Inside of static
, create a file called style.css
.
Here's some default styling for you:
li { font-size: 25px; } h1, p { text-align: center; } body::after { content: ""; background: url(http://cinematically.net/wp-content/uploads/2014/08/toys_poster.jpg); opacity: 0.3; top: 0; left: 0; bottom: 0; right: 0; position: absolute; z-index: -1; }
(Want to know what's going on with that body::after
selector? Check out this article.)
Finally, we need to link to our stylesheet from inside of base.html
:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>CRUD Time</title> <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}"> </head> <body> {% block content %} {% endblock %} </body> </html>
Beautiful!
If you'd like to see an example of building a CRUD application with a list, feel free to watch the screencast below.
When you're ready, move on to Flask Fundamentals Exercises