Professional Documents
Culture Documents
Flask Web Development-195-213
Flask Web Development-195-213
In recent years, there has been a trend in web applications to move more and more of
the business logic to the client side, producing an architecture that is known as Rich
Internet Application (RIA). In RIAs, the server’s main (and sometimes only) function
is to provide the client application with data retrieval and storage services. In this model,
the server becomes a web service or Application Programming Interface (API).
There are several protocols by which RIAs can communicate with a web service. Remote
Procedure Call (RPC) protocols such as XML-RPC or its derivative Simplified Object
Access Protocol (SOAP) were popular choices a few years ago. More recently, the Rep‐
resentational State Transfer (REST) architecture has emerged as the favorite for web
applications due to it being built on the familiar model of the World Wide Web.
Flask is an ideal framework to build RESTful web services due to its lightweight nature.
In this chapter, you will learn how to implement a Flask-based RESTful API.
Introduction to REST
Roy Fielding’s Ph.D. dissertation introduces the REST architectural style for web serv‐
ices by listing its six defining characteristics:
Client-Server
There must be a clear separation between the clients and the server.
Stateless
A client request must contain all the information that is necessary to carry it out.
The server must not store any state about the client that persists from one request
to the next.
175
Cache
Responses from the server can be labeled as cacheable or noncacheable so that
clients (or intermediaries between clients and servers) can use a cache for optimi‐
zation purposes.
Uniform Interface
The protocol by which clients access server resources must be consistent, well de‐
fined, and standardized. The commonly used uniform interface of REST web serv‐
ices is the HTTP protocol.
Layered System
Proxy servers, caches, or gateways can be inserted between clients and servers as
necessary to improve performance, reliability, and scalability.
Code-on-Demand
Clients can optionally download code from the server to execute in their context.
Be aware that Flask applies special treatment to routes that end with
a slash. If a client requests a URL without a trailing slash and the only
matching route has a slash at the end, then Flask will automatically
respond with a redirect to the trailing slash URL. No redirects are
issued for the reverse case.
The REST architecture does not require that all methods be imple‐
mented for a resource. If the client invokes a method that is not
supported for a given resource, then a response with the 405 status
code for “Method Not Allowed” should be returned. Flask handles
this error automatically.
Note how the url, author, and comments fields in the blog post above are fully qualified
resource URLs. This is important because these URLs allow the client to discover new
resources.
In a well-designed RESTful API, the client just knows a short list of top-level resource
URLs and then discovers the rest from links included in responses, similar to how you
can discover new web pages while browsing the Web by clicking on links that appear
in pages that you know.
Versioning
In a traditional server-centric web application, the server has full control of the appli‐
cation. When an application is updated, installing the new version in the server is
enough to update all users because even the parts of the application that run in the user’s
web browser are downloaded from the server.
The situation with RIAs and web services is more complicated, because often clients are
developed independently of the server—maybe even by different people. Consider the
case of an application where the RESTful web service is used by a variety of clients
including web browsers and native smartphone clients. The web browser client can be
updated in the server at any time, but the smartphone apps cannot be updated by force;
the smartphone owner needs to allow the update to happen. Even if the smartphone
owner is willing to update, it is not possible to time the deployment of the updated
smartphone applications to all the app stores to coincide exactly with the deployment
of the new server.
For these reasons, web services need to be more tolerant than regular web applications
and be able to work with old versions of its clients. A common way to address this
problem is to version the URLs handled by the web service. For example, the first release
of the blogging web service could expose the collection of blog posts at /api/v1.0/posts/.
Including the web service version in the URL helps keeps old and new features organized
so that the server can provide new features to new clients while continuing to support
Note how the package used for the API includes a version number in its name. When
a backward-incompatible version of the API needs to be introduced, it can be added as
another package with a different version number and both APIs can be served at the
same time.
Error Handling
A RESTful web service informs the client of the status of a request by sending the ap‐
propriate HTTP status code in the response plus any additional information in the
response body. The typical status codes that a client can expect to see from a web service
are listed in Table 14-2.
Table 14-2. HTTP response status codes typically returned by APIs
HTTP status Name Description
code
200 OK The request was completed successfully.
201 Created The request was completed successfully and a new resource was created as a result.
400 Bad request The request is invalid or inconsistent.
401 Unauthorized The request does not include authentication information.
403 Forbidden The authentication credentials sent with the request are insufficient for the request.
404 Not found The resource referenced in the URL was not found.
405 Method not allowed The request method requested is not supported for the given resource.
500 Internal server error An unexpected error has occurred while processing the request.
The handling of status codes 404 and 500 presents a small complication, in that these
errors are generated by Flask on its own and will usually return an HTML response,
which is likely to confuse an API client.
This new version of the error handler checks the Accept request header, which Werk‐
zeug decodes into request.accept_mimetypes, to determine what format the client
wants the response in. Browsers generally do not specify any restrictions on response
formats, so the JSON response is generated only for clients that accept JSON and do not
accept HTML.
The remaining status codes are generated explicitly by the web service, so they can be
implemented as helper functions inside the blueprint in the errors.py module.
Example 14-5 shows the implementation of the 403 error; the others are similar.
Example 14-5. app/api/errors.py: API error handler for status code 403
def forbidden(message):
response = jsonify({'error': 'forbidden', 'message': message})
response.status_code = 403
return response
Now view functions in the web service can invoke these auxiliary functions to generate
error responses.
Because the RESTful architecture is based on the HTTP protocol, HTTP authentica‐
tion is the preferred method used to send credentials, either in its Basic or Digest flavors.
With HTTP authentication, user credentials are included in an Authorization header
with all requests.
The HTTP authentication protocol is simple enough that it can be implemented directly,
but the Flask-HTTPAuth extension provides a convenient wrapper that hides the pro‐
tocol details in a decorator similar to Flask-Login’s login_required.
Flask-HTTPAuth is installed with pip:
(venv) $ pip install flask-httpauth
To initialize the extension for HTTP Basic authentication, an object of class
HTTPBasicAuth must be created. Like Flask-Login, Flask-HTTPAuth makes no as‐
sumptions about the procedure required to verify user credentials, so this information
is given in a callback function. Example 14-6 shows how the extension is initialized and
provided with a verification callback.
Example 14-6. app/api_1_0/authentication.py: Flask-HTTPAuth initialization
from flask.ext.httpauth import HTTPBasicAuth
auth = HTTPBasicAuth()
@auth.verify_password
def verify_password(email, password):
if email == '':
g.current_user = AnonymousUser()
return True
user = User.query.filter_by(email = email).first()
if not user:
return False
g.current_user = user
return user.verify_password(password)
When the authentication credentials are invalid, the server returns a 401 error to the
client. Flask-HTTPAuth generates a response with this status code by default, but to
ensure that the response is consistent with other errors returned by the API, the error
response can be customized as shown in Example 14-7.
Example 14-7. _app/api_1_0/authentication.py: Flask-HTTPAuth error handler
@auth.error_handler
def auth_error():
return unauthorized('Invalid credentials')
@api.before_request
@auth.login_required
def before_request():
Now the authentication checks will be done automatically for all the routes in the blue‐
print. As an additional check, the before_request handler also rejects authenticated
users who have not confirmed their accounts.
Token-Based Authentication
Clients must send authentication credentials with every request. To avoid having to
constantly transfer sensitive information, a token-based authentication solution can be
offered.
In token-based authentication, the client sends the login credentials to a special URL
that generates authentication tokens. Once the client has a token it can use it in place
of the login credentials to authenticate requests. For security reasons, tokens are issued
with an associated expiration. When a token expires, the client must reauthenticate to
get a new one. The risk of a token getting in the wrong hands is limited due to its short
lifespan. Example 14-9 shows the two new methods added to the User model that sup‐
port generation and verification of authentication tokens using itsdangerous.
Example 14-9. app/models.py: Token-based authentication support
class User(db.Model):
# ...
def generate_auth_token(self, expiration):
s = Serializer(current_app.config['SECRET_KEY'],
expires_in=expiration)
return s.dumps({'id': self.id})
@staticmethod
def verify_auth_token(token):
s = Serializer(current_app.config['SECRET_KEY'])
try:
data = s.loads(token)
except:
return None
return User.query.get(data['id'])
The generate_auth_token() method returns a signed token that encodes the user’s id
field. An expiration time given in seconds is also used. The verify_auth_token()
method takes a token and, if found valid, it returns the user stored in it. This is a static
method, as the user will be known only after the token is decoded.
To authenticate requests that come with a token, the verify_password callback for
Flask-HTTPAuth must be modified to accept tokens as well as regular credentials. The
updated callback is shown in Example 14-10.
In this new version, the first authentication argument can be the email address or an
authentication token. If this field is blank, an anonymous user is assumed, as before. If
the password is blank, then the email_or_token field is assumed to be a token and
validated as such. If both fields are nonempty then regular email and password authen‐
tication is assumed. With this implementation, token-based authentication is optional;
it is up to each client to use it or not. To give view functions the ability to distinguish
between the two authentication methods a g.token_used variable is added.
The route that returns authentication tokens to the client is also added to the API blue‐
print. The implementation is shown in Example 14-11.
Example 14-11. app/api_1_0/authentication.py: Authentication token generation
@api.route('/token')
def get_token():
if g.current_user.is_anonymous() or g.token_used:
return unauthorized('Invalid credentials')
return jsonify({'token': g.current_user.generate_auth_token(
expiration=3600), 'expiration': 3600})
Since this route is in the blueprint, the authentication mechanisms added to the
before_request handler also apply to it. To prevent clients from using an old token to
request a new one, the g.token_used variable is checked, and in that way requests
authenticated with a token can be rejected. The function returns a token in the JSON
response with a validity period of one hour. The period is also included in the JSON
response.
The url, author, and comments fields need to return the URLs for the respective re‐
sources, so these are generated with url_for() calls to routes that will be defined in the
API blueprint. Note that _external=True is added to all url_for() calls so that fully
qualified URLs are returned instead of the relative URLs that are typically used within
the context of a traditional web application.
This example also shows how it is possible to return “made-up” attributes in the rep‐
resentation of a resource. The comment_count field returns the number of comments
that exist for the blog post. Although this is not a real attribute of the model, it is included
in the resource representation as a convenience to the client.
The to_json() method for User models can be constructed in a similar way to Post.
This method is shown in Example 14-13.
Example 14-13. app/models.py: Convert a user to a JSON serializable dictionary
class User(UserMixin, db.Model):
# ...
def to_json(self):
json_user = {
'url': url_for('api.get_post', id=self.id, _external=True),
'username': self.username,
'member_since': self.member_since,
'last_seen': self.last_seen,
'posts': url_for('api.get_user_posts', id=self.id, _external=True),
'followed_posts': url_for('api.get_user_followed_posts',
id=self.id, _external=True),
Note how in this method some of the attributes of the user, such as email and role, are
omitted from the response for privacy reasons. This example again demonstrates that
the representation of a resource offered to clients does not need to be identical to the
internal representation of the corresponding database model.
Converting a JSON structure to a model presents the challenge that some of the data
coming from the client might be invalid, wrong, or unnecessary. Example 14-14 shows
the method that creates a Post model from JSON.
Example 14-14. app/models.py: Create a blog post from JSON
from app.exceptions import ValidationError
class Post(db.Model):
# ...
@staticmethod
def from_json(json_post):
body = json_post.get('body')
if body is None or body == '':
raise ValidationError('post does not have a body')
return Post(body=body)
As you can see, this implementation chooses to only use the body attribute from the
JSON dictionary. The body_html attribute is ignored since the server-side Markdown
rendering is automatically triggered by a SQLAlchemy event whenever the body at‐
tribute is modified. The timestamp attribute does not need to be given, unless the client
is allowed to backdate posts, which is not a feature of this application. The author field
is not used because the client has no authority to select the author of a blog post; the
only possible value for the author field is that of the authenticated user. The comments
and comment_count attributes are automatically generated from a database relationship,
so there is no useful information in them that is needed to create a model. Finally, the
url field is ignored because in this implementation the resource URLs are defined by
the server, not the client.
Note how error checking is done. If the body field is missing or empty then a
ValidationError exception is raised. Throwing an exception is in this case the appro‐
priate way to deal with the error because this method does not have enough knowledge
to properly handle the condition. The exception effectively passes the error up to the
caller, enabling higher level code to do the error handling. The ValidationError class
is implemented as a simple subclass of Python’s ValueError. This implementation is
shown in Example 14-15.
The application now needs to handle this exception by providing the appropriate re‐
sponse to the client. To avoid having to add exception catching code in view functions,
a global exception handler can be installed. A handler for the ValidationError excep‐
tion is shown in Example 14-16.
Example 14-16. app/api_1_0/errors.py: API error handler for ValidationError excep‐
tions
@api.errorhandler(ValidationError)
def validation_error(e):
return bad_request(e.args[0])
The errorhandler decorator is the same one that is used to register handlers for HTTP
status codes, but in this usage it takes an Exception class as argument. The decorated
function will be invoked any time an exception of the given class is raised. Note that the
decorator is obtained from the API blueprint, so this handler will be invoked only when
the exception is raised while a route from the blueprint is being processed.
Using this technique, the code in view functions can be written very cleanly and con‐
cisely, without the need to include error checking. For example:
@api.route('/posts/', methods=['POST'])
def new_post():
post = Post.from_json(request.json)
post.author = g.current_user
db.session.add(post)
db.session.commit()
return jsonify(post.to_json())
@api.route('/posts/<int:id>')
@auth.login_required
The first route handles the request of the collection of posts. This function uses a list
comprehension to generate the JSON version of all the posts. The second route returns
a single blog post and responds with a code 404 error when the given id is not found
in the database.
The handler for 404 errors is at the application level, but it will pro‐
vide a JSON response if the client requests that format. If a response
customized to the web service is desired, the 404 error handler can
be overridden in the blueprint.
The POST handler for blog post resources inserts a new blog post in the database. This
route is shown in Example 14-18.
Example 14-18. app/api_1_0/posts.py: POST resource handler for posts
@api.route('/posts/', methods=['POST'])
@permission_required(Permission.WRITE_ARTICLES)
def new_post():
post = Post.from_json(request.json)
post.author = g.current_user
db.session.add(post)
db.session.commit()
return jsonify(post.to_json()), 201, \
{'Location': url_for('api.get_post', id=post.id, _external=True)}
The PUT handler for blog posts, used for editing existing resources, is shown in
Example 14-20.
Example 14-20. app/api_1_0/posts.py: PUT resource handler for posts
@api.route('/posts/<int:id>', methods=['PUT'])
@permission_required(Permission.WRITE_ARTICLES)
def edit_post(id):
post = Post.query.get_or_404(id)
if g.current_user != post.author and \
not g.current_user.can(Permission.ADMINISTER):
return forbidden('Insufficient permissions')
post.body = request.json.get('body', post.body)
db.session.add(post)
return jsonify(post.to_json())
The permission checks are more complex in this case. The standard check for permis‐
sion to write blog posts is done with the decorator, but to allow a user to edit a blog post
the function must also ensure that the user is the author of the post or else is an ad‐
ministrator. This check is added explicitly to the view function. If this check had to be
added in many view functions, building a decorator for it would be a good way to avoid
code repetition.
Since the application does not allow deletion of posts, the handler for the DELETE request
method does not need to be implemented.
The resource handlers for users and comments are implemented in a similar way.
Table 14-3 lists the set of resources implemented for this application. The complete
implementation is available for you to study in the GitHub repository.
Table 14-3. Flasky API resources
Resource URL Methods Description
/users/<int:id> GET A user
/users/<int:id>/posts/ GET The blog posts written by a user
/users/<int:id>/timeline/ GET The blog posts followed by a user
/posts/ GET, POST All the blog posts
Note that the resources that were implemented enable a client to offer a subset of the
functionality that is available through the web application. The list of supported re‐
sources could be expanded if necessary, such as to expose followers, to enable comment
moderation, and to implement any other features that a client might need.
The posts field in the JSON response contains the data items as before, but now it is
just a portion of the complete set. The prev and next items contain the resource URLs
for the previous and following pages, when available. The count value is the total number
of items in the collection.
This technique can be applied to all the routes that return collections.
{
"posts": [
...
],
"prev": null
"next": "http://127.0.0.1:5000/api/v1.0/posts/?page=2",
"count": 150
}
Note the pagination links included in the response. Since this is the first page, a previous
page is not defined, but a URL to obtain the next page and a total count were returned.
The same request can be issued by an anonymous user by sending an empty email and
password:
(venv) $ http --json --auth : GET http://127.0.0.1:5000/api/v1.0/posts/
The following command sends a POST request to add a new blog post:
(venv) $ http --auth <email>:<password> --json POST \
> http://127.0.0.1:5000/api/v1.0/posts/ \
> "body=I'm adding a post from the *command line*."
HTTP/1.0 201 CREATED
Content-Length: 360
Content-Type: application/json
Date: Sun, 22 Dec 2013 08:30:27 GMT
Location: http://127.0.0.1:5000/api/v1.0/posts/111
Server: Werkzeug/0.9.4 Python/2.7.3
{
"expiration": 3600,
"token": "eyJpYXQiOjEzODg4MjQ3MjcsImV4cCI6MTM4ODgyODMyNywiYWxnIjoiSFMy..."
}
And now the returned token can be used to make calls into the API for the next hour
by passing it along with an empty password field:
(venv) $ http --json --auth eyJpYXQ...: GET http://127.0.0.1:5000/api/v1.0/posts/
When the token expires, requests will be returned with a code 401 error, indicating that
a new token needs to be obtained.
Congratulations! This chapter completes Part II, and with that the feature development
phase of Flasky is complete. The next step is obviously to deploy it, and that brings a
new set of challenges that are the subject of Part III.