Flask REST and JSON APIs
February 03, 2020
The goals of this lecture include:
- Review GET vs. POST
- Review other HTTP verbs (PUT, PATCH, DELETE)
- Describe what REST is
- Build and test JSON APIs
Review of HTTP Verbs
Up to this point, we've dealt primarily with GET
and POST
requests.
GET
Recall that GET
is the HTTP verb that we use when we want to get information from the server.
Some other things to note about GET
:
- Data Sent via Query String. Any information for our
GET
request is passed via query string in the URL. - Cacheable.
GET
requests can be cached and stay in the browser's history. They can also be bookmarked since all relevant information is stored in the URL itself. - Repeatable. If we perform the same
GET
request more than once, we can expect the same response.
POST
POST
is the verb we reach for when we'd like to send information to the server for the purposes of creating a record or otherwise making some sort of change to data.
Some things to note about POST
:
- Data Sent in Body. Details about our
POST
request are sent within the body of the request. - Not Cacheable. Since data is sent in the body of the request, we cannot cache or bookmark a
POST
request because this information is no longer accessible after completion. - Not Repeatable. We deem
POST
requests not repeatable because every time wePOST
, the effect on the server will be different.
When to Use GET vs POST
We use GET
to retrieve data and POST
to create data.
Consider the following examples:
- Searching / Filtering GET
- Sending an Email POST
- Updating a User ???
For updates to (and removal of) our data, we're going to introduce three new HTTP verbs: PUT
, PATCH
and DELETE
.
PUT, PATCH and DELETE
When dealing with updates to our data, we have the choice between using either PUT
or PATCH
.
PUT
is used when we'd like to replace the entire resource or record in the database.
PATCH
is used when we'd only like to update certain values associated with a resource or record.
For example, say we have an endpoint where users can update their profile. If a user makes a modification, like changing their username and/or date of birth, we would use a PATCH
request since our action is only updating part of the total user record.
Alternatively, if we wanted to replace the entire record, we would use PUT
.
Finally, as expected, DELETE
deletes a record.
PUT
is idempotent, meaning that if we perform the same request twice, we will get the exact same result. This is helpful to consider when dealing with requests that are made but could fail: if the user isn't sure the request failed or not, performing the request again will have no ill effect as a consequence.
Safety and Idempotence
An operation (or HTTP verb) is considered safe if by using it, there is no data changed on the server we're communicating with as a result.
An idempotent action is one that, if repeated, will result in the exact same outcome. If we made the same call ten times, the result to the data on the server would be the same as if we had only called it once.
Why is Safety and Idempotence Relevant?
Understanding the meaning behind both safe and idempotent when it comes to HTTP verbs gives us a better understanding of existing standards around how developers define and build routes.
They are a core part of the REST standard!
Introduction to REST
As developers, part of our job is to make good decisions about the architecture of the applications we work on.
Routes are an essential part of this and an area in which we have a great amount of flexibility to shape as we see fit.
But, remember that one of our top priorities should be to make our code as predictable and easily understood as possible (both for future us and others).
Lucky for us, there is an agreed-upon standard for route architecture: REST.
What is REST?
REST is an architectural style that defines standards for route design, from the endpoint format to the behavior that follows.
Such behaviors include the client-server model, statelessness and cacheability.
The APIs that adhere to these standards are deemed "RESTful".
RESTful Format
Let's take a tour of a RESTful API.
Base URL
RESTful APIs usually have a base URL that indicates the service you're communicating with is an API.
Sometimes that is a subdomain, other times it's a dedicated endpoint:
http://api.site.com/
or http://site.com/api/
Resource
After the base URL, what follows is often a resource. We can think of a resource as an entity or category of records.
In the case below, books
would be a resource:
http://api.site.com/books/
We can think of resources in the same way that we think of classes in OOP: objects with a specific type, associated data and methods, and relationships with other objects in our program.
HTTP Verbs
For any endpoint belonging to our RESTful API, we can use any of our verbs: GET
, POST
, PUT
, PATCH
, and DELETE
.
When our verbs meet with an endpoint, because of the agreed upon standard of REST, we as developers can immediately intuit what would happen as a result of that call.
Going back to our OOP analogy, our HTTP verbs work in the same way that our class methods do.
A DELETE
request to the endpoint /cats/fluffy
is same idea as calling fluffy.delete()
.
An Example Application
If we had an application about snacks, our RESTful routing might look like the following:
HTTP Verb | Route | Meaning |
---|---|---|
GET | /snacks | Get all snacks |
GET | /snacks/[id] | Get snack |
POST | /snacks | Create snack |
PUT/PATCH | /snacks/[id] | Update snack |
DELETE | /snacks/[id] | Delete snack |
RESTful Route Responses
Not completely standardized, but below is what we could reasonably expect from each endpoint as a response to our request:
- GET /snacks. 200 OK, with JSON describing snacks
- GET /snacks/[id]. 200 OK, with JSON describing single snack
- POST /snacks. 201 CREATED, with JSON describing new snack
- PUT or PATCH /snacks/[id]. 200 OK, with JSON describing updated snack
- DELETE /snacks/[id]. 200 OK, with JSON describing success
Nested Routes
With larger applications, it is often the case that the relationships between resources are more numerous and complex.
For example, say we have a Yelp-like service that offers users information on businesses in their area.
In our routes, we'd have an endpoint called /businesses
, at which we could find all businesses listed, and /businesses/[business-id]
where we could find details for a specific business.
We're in the review business, however, and each business has reviews associated with it. How might we write our route for our reviews
resource, considering this relationship?
Since reviews
are associated with a particular business, we might append that resource to our individual business route, as seen below:
/businesses/[business-id]/reviews
And if we wanted to access a specific review:
/businesses/[business-id]/reviews/[review-id]
The HTTP verbs associated with each endpoint should behave exactly as we've already seen.. because RESTful routing, of course!
RESTful APIs with Flask
Back in Flask land, we can now incorporate our new knowledge about RESTful routing. This time around, however, we'll be returning JSON -- not HTML as we have up until this point.
jsonify()
To return JSON from our endpoints, we're going to use the built-in Flask function called jsonify
. jsonify
will take our data structure and turn it into -- you guessed it -- JSON.
But jsonify
can't convert just any old thing to JSON. In fact, JSON can only represent dictionaries, lists and primitive types. If we try to jsonify
a SQLAlchemy model, we're not going to get the response we were hoping for.
Python can’t just turn our objects into JSON. We'll have to first employ a process called serialization.
Serialization
Using serialization, we take it upon ourselves to convert our unique data structures into dictionaries or lists that feature only the information we're interested in:
def serialize_dessert(dessert):
"""Serialize a dessert SQLAlchemy obj to dictionary."""
return {
"id": dessert.id,
"name": dessert.name,
"calories": dessert.calories,
}
Meanwhile, back in our route, before we send back the response the user has requested, we first make sure that we're serializing each dessert and then converting the list to JSON:
@app.route("/desserts")
def list_all_desserts():
"""Return JSON {'desserts': [{id, name, calories}, ...]}"""
desserts = Dessert.query.all()
serialized = [serialize_dessert(d) for d in desserts]
return jsonify(desserts=serialized)
And in the case that we only have one instance to serialize:
@app.route("/desserts/<dessert_id>")
def list_single_dessert(dessert_id):
"""Return JSON {'dessert': {id, name, calories}}"""
dessert = Dessert.query.get(dessert_id)
serialized = serialize_dessert(dessert)
return jsonify(dessert=serialized)
Sending Data to a Flask JSON API
Of the ways we've been interacting with APIs so far, the following outlines how to send data to the JSON APIs we'll create:
- Insomnia. Choose JSON as the request type.
- cURL. Set the Content-Type header:
$ curl localhost:5000/api/desserts \
-H "Content-Type: application/json" \
-d '{"name":"chocolate bar","calories": 200}'
(Makes a POST to /api/desserts, passing in that JSON data)
- axios. For AJAX using Axios, sending JSON is the default.
Receiving Data in a Flask JSON API
A request made with Content-Type: application/json
won't be available in request.args
or request.form
, but rather inside request.json
.
Testing our API
Since we're now working with APIs that return JSON instead of HTML, we're no longer going to be testing the HTML that is returned from our endpoints.
You can imagine this will make things much easier for us: we're just testing data!
Testing data not only makes our tests easier to maintain, but also allows us to experiment with the endpoints we're testing (using Insomnia or curl) while we're working on them.
Take the following test, where we're interested in ensuring our route to GET
all desserts behaves as we'd expect:
def test_all_desserts(self):
with app.test_client() as client:
resp = client.get("/desserts")
self.assertEqual(resp.status_code, 200)
self.assertEqual(
resp.json,
{'desserts': [{
'id': self.dessert_id,
'name': 'TestCake',
'calories': 10
}]})
And, if we add a new record using POST
, that the response we get back matches with the dessert we've just created:
def test_create_dessert(self):
with app.test_client() as client:
resp = client.post(
"/desserts", json={
"name": "TestCake2",
"calories": 20,
})
self.assertEqual(resp.status_code, 201)
# don't know what ID it will be, so test then remove
self.assertIsInstance(resp.json['dessert']['id'], int)
del resp.json['dessert']['id']
self.assertEqual(
resp.json,
{"dessert": {'name': 'TestCake2', 'calories': 20}})
self.assertEqual(Dessert.query.count(), 2)