- Build an API to handle POST, PATCH, and DELETE requests.
- Application Programming Interface (API): a software application that allows two or more software applications to communicate with one another. Can be standalone or incorporated into a larger product.
- HTTP Request Method: assets of HTTP requests that tell the server which actions the client is attempting to perform on the located resource.
GET
: the most common HTTP request method. Signifies that the client is attempting to view the located resource.POST
: the second most common HTTP request method. Signifies that the client is attempting to submit a form to create a new resource.PATCH
: an HTTP request method that signifies that the client is attempting to update a resource with new information.DELETE
: an HTTP request method that signifies that the client is attempting to delete a resource.
So far, we've seen how to set up an API with Flask to allow frontend applications to access data from a database in a JSON format. For many applications, just being able to access/read data isn't enough — what kind of app would Twitter be if you couldn't write posts? What would Instagram be if you couldn't like photos? How embarrassing would Facebook be if you couldn't go back and delete those regrettable high school photos?
All of those applications, and most web apps, can be broadly labeled as CRUD applications — they allow users to Create, Read, Update, and Delete information.
We've seen a few ways to Read data in an API. We've also already seen how to Create/Update/Delete records from a database using SQLAlchemy. All that's left is to connect what we know from SQLAlchemy with some new techniques for establishing routes and accessing data in our Flask application.
We'll continue working on the game review application from the previous lessons. To get set up, run:
$ pipenv install; pipenv shell
$ cd server
$ flask db upgrade
$ python seed.py
You can view the models in the server/models.py
module, and the migrations in the
server/migrations/versions
directory. Here's what the relationships will look
like in our ERD:
server/app.py
has also been configured with GET
routes for all Review
and
User
records.
Now, run the server with Flask and re-explore some of the routes from our GET
lesson and the new /reviews
and /users
routes:
$ python app.py
With that set up, let's start working on some CRUD!
Let's start with the simplest action: the DELETE request. Imagine we're building a new feature in our frontend React application. Our users want some way to delete their reviews, in case they change their minds. In React, our component for handling this delete action might look something like this:
// example code
function ReviewItem({ review, onDeleteReview }) {
function handleDeleteClick() {
fetch(`http://localhost:9292/reviews/${review.id}`, {
method: "DELETE",
})
.then((r) => r.json())
.then((deletedReview) => onDeleteReview(deletedReview));
}
return (
<div>
<p>Score: {review.score}</p>
<p>{review.comment}</p>
<button onClick={handleDeleteClick}>Delete Review</button>
</div>
);
}
So, it looks like our server needs to handle a few new things:
- Handle requests with the
DELETE
HTTP verb to/reviews/<int:id>
. - Find the review to delete using the ID.
- Delete the review from the database.
- Send a response with the deleted review as JSON to confirm that it was deleted successfully, so the frontend can show the successful deletion to the user.
Let's take things one step at a time. First, we'll need to handle requests by adding a new route in the controller. We can write out a route for a DELETE request just like we would for a GET request, just by changing the method:
# server/app.py
# imports, config, games, game_by_id, reviews
@app.route('/reviews/<int:id>', methods=['GET', 'DELETE'])
def review_by_id(id):
review = Review.query.filter(Review.id == id).first()
if request.method == 'GET':
review_dict = review.to_dict()
response = make_response(
review_dict,
200
)
return response
elif request.method == 'DELETE':
db.session.delete(review)
db.session.commit()
response_body = {
"delete_successful": True,
"message": "Review deleted."
}
response = make_response(
response_body,
200
)
return response
Let's review the new content:
- The
@app.route
decorator acceptsmethods
as a default argument. This is simply a list of accepted methods as strings. By default, this list only contains'GET'
. - The request context allows us to access the HTTP method used by the request
and control flow from there. If the request's method is
GET
, we perform the same actions that we did in the/games/<int:id>
route. If the method isDELETE
, we delete the resource. - Unsupported methods will receive a 405 response code by default. This means "Method Not Allowed".
Now, the question on everyone's minds: how do we actually send a DELETE
request?
We all know from experience that we can delete resources on the internet: emails, Instagram posts with unmade beds in the background, etc. There are many different actions that can be carried out over the internet, and browsers are actually very flexible on this front- it's HTML that isn't.
The W3C specifications for HTML say that it should only support GET
and
POST
. This means that any requests sent to the API through the browser with
unsupported methods will have these methods cast to GET
...which will then be
used to update and delete resources. (We don't want that!)
Because of these limitations, we haven't explored too many HTTP methods with Flask up to this point.
We can use Python scripts and applications with libraries like requests
to
test our DELETE
resource, but it is often easier to just use Postman.
NOTE: Requests to
localhost
or127.0.0.1
can only be sent using the Postman desktop client. This is unsupported in the cloud.
Sending a GET
request (the default method) for the first review will return
something similar to the following:
I know what you're thinking: how could we possibly delete this illuminating review of "All lawyer as teacher world any"? How will anyone know that "Republican help young large treatment note"?
Alas, we need to make sure our API works. Change the request method to DELETE
with the dropdown menu to the left of the search bar and submit a new request to
this resource.
NOTE: You may notice that the port accessed here has switched to 5001. You can run whichever port you like on your machine, as long as there isn't a conflict with another application. We typically use 5555 since it is the easiest 5000s port to remember after 5000, which sees the AirPlay conflict on MacOS.
Navigate back to the same resource with GET
and you should see a 500 Internal Server Error
. There's no resource here anymore! This is obviously not
the most helpful message, but it can be handled with control flow just like any
other error. A simple solution will be included in the solution code at the end
of this lesson.
For our next feature, let's give our users the ability to Create new reviews. From the frontend, here's how our React component might look:
// example code
function ReviewForm({ userId, gameId, onAddReview }) {
const [comment, setComment] = useState("");
const [score, setScore] = useState("0");
function handleSubmit(e) {
e.preventDefault();
fetch("http://localhost:9292/reviews", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
comment: comment,
score: score,
user_id: userId,
game_id: gameId,
}),
})
.then((r) => r.json())
.then((newReview) => onAddReview(newReview));
}
return <form onSubmit={handleSubmit}>{/* controlled form code here*/}</form>;
}
This request is a bit trickier than the last: in order to create a review in the database, we need some way of getting all the data that the user entered into the form. From the code above, you can see that we'll have access to that data in the body of the request, as a JSON-formatted string. So in terms of the steps for our server, we need to:
- Handle requests with the
POST
HTTP verb to/reviews
. - Access the data in the body of the request.
- Use that data to create a new review in the database.
- Send a response with newly created review as JSON.
Let's start with the easy part. We can create a workflow for a new method like so:
#server/app.py
# imports, config, games, game_by_id
@app.route('/reviews', methods=['GET', 'POST'])
def reviews():
if request.method == 'GET':
reviews = []
for review in Review.query.all():
review_dict = review.to_dict()
reviews.append(review_dict)
response = make_response(
reviews,
200
)
return response
elif request.method == 'POST':
response_body = {}
response = make_response(
response_body,
201
)
return response
NOTE: A 201 status code means that a record has been successfully created.
In this new block, we'll need to create a new record using the attributes passed in the request.
# server/app.py
# imports, config, games, game_by_id
@app.route('/reviews', methods=['GET', 'POST'])
def reviews():
if request.method == 'GET':
reviews = []
for review in Review.query.all():
review_dict = review.to_dict()
reviews.append(review_dict)
response = make_response(
reviews,
200
)
return response
elif request.method == 'POST':
new_review = Review(
score=request.form.get("score"),
comment=request.form.get("comment"),
game_id=request.form.get("game_id"),
user_id=request.form.get("user_id"),
)
db.session.add(new_review)
db.session.commit()
review_dict = new_review.to_dict()
response = make_response(
review_dict,
201
)
return response
The request context has access to form data, among many other things. While we
haven't created a form here, makeshift forms can still be attached to requests
and their attributes can be parsed to create new records. It's important that
we create review_dict
after committing the review to the database, as this
populates it with an ID and data from its game and user. Submitting a POST
request with Postman should return something like this:
Now that we've created, read, and deleted data, let's look at how to update.
Onto the last HTTP verb for this unit: PATCH
! Now that you've learned about
POST
and DELETE
requests, this should be more straightforward. From the
frontend, we might need to use a PATCH
request to handle a feature that would
allow a user to update their review, in case they change their minds:
function EditReviewForm({ review, onUpdateReview }) {
const [comment, setComment] = useState("");
const [score, setScore] = useState("0");
function handleSubmit(e) {
e.preventDefault();
fetch(`http://localhost:9292/reviews/${review.id}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
comment: comment,
score: score,
}),
})
.then((r) => r.json())
.then((updatedReview) => onUpdateReview(updatedReview));
}
return <form onSubmit={handleSubmit}>{/* controlled form code here*/}</form>;
}
The steps we'll need to handle on the server for this request are basically a combination of DELETE and POST. We'll need to:
- Handle requests with the
PATCH
HTTP verb to/reviews/<int:id>
. - Find the review to update using the ID.
- Access the data in the body of the request.
- Use that data to update the review in the database.
- Send a response with updated review as JSON.
Give it a shot yourself before looking at the solution! You have all the tools you need to get this request working. When you're ready, keep scrolling...
...
...
...
...
...
...
Ok, here's how the code for this route would look:
# server/app.py
# imports, config, games, game_by_id, reviews
@app.route('/reviews/<int:id', methods=['GET', 'PATCH', 'DELETE'])
# GET
elif request.method == 'PATCH':
review = Review.query.filter(Review.id == id).first()
for attr in request.form:
setattr(review, attr, request.form.get(attr))
db.session.add(review)
db.session.commit()
review_dict = review.to_dict()
response = make_response(
review_dict,
200
)
return response
# DELETE
- First, we locate the record we want to change.
- Second, we update the record's attributes using
request.form
. We usesetattr()
here because it allows us to use variable values as attribute names- when we don't know which fields are being updated, this is important. - From that point, this is very similar to our
POST
block from'/reviews'
. We need to save the updated record to the database and then serve it to the client as JSON.
Run a PATCH
request in Postman and you should see something similar to the
following:
You're at the point now where you can create a JSON API that handles all four CRUD actions: Create, Read, Update, and Delete. With just these four actions, you can build an API for almost any application you can think of!
# server/app.py
from flask import Flask, jsonify, request, make_response
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from models import db, User, Review, Game
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.json.compact = False
migrate = Migrate(app, db)
db.init_app(app)
@app.route('/')
def index():
return "Index for Game/Review/User API"
@app.route('/games')
def games():
games = []
for game in Game.query.all():
game_dict = game.to_dict()
games.append(game_dict)
response = make_response(
games,
200
)
return response
@app.route('/games/<int:id>')
def game_by_id(id):
game = Game.query.filter(Game.id == id).first()
game_dict = game.to_dict()
response = make_response(
game_dict,
200
)
return response
@app.route('/reviews', methods=['GET', 'POST'])
def reviews():
if request.method == 'GET':
reviews = []
for review in Review.query.all():
review_dict = review.to_dict()
reviews.append(review_dict)
response = make_response(
reviews,
200
)
return response
elif request.method == 'POST':
new_review = Review(
score=request.form.get("score"),
comment=request.form.get("comment"),
game_id=request.form.get("game_id"),
user_id=request.form.get("user_id"),
)
db.session.add(new_review)
db.session.commit()
review_dict = new_review.to_dict()
response = make_response(
review_dict,
201
)
return response
@app.route('/reviews/<int:id>', methods=['GET', 'PATCH', 'DELETE'])
def review_by_id(id):
review = Review.query.filter(Review.id == id).first()
if review == None:
response_body = {
"message": "This record does not exist in our database. Please try again."
}
response = make_response(jsonify(response_body), 404)
return response
else:
if request.method == 'GET':
review_dict = review.to_dict()
response = make_response(
review_dict,
200
)
return response
elif request.method == 'PATCH':
review = Review.query.filter(Review.id == id).first()
for attr in request.form:
setattr(review, attr, request.form.get(attr))
db.session.add(review)
db.session.commit()
review_dict = review.to_dict()
response = make_response(
review_dict,
200
)
return response
elif request.method == 'DELETE':
db.session.delete(review)
db.session.commit()
response_body = {
"delete_successful": True,
"message": "Review deleted."
}
response = make_response(
response_body,
200
)
return response
@app.route('/users')
def users():
users = []
for user in User.query.all():
user_dict = user.to_dict()
users.append(user_dict)
response = make_response(
users,
200
)
return response
if __name__ == '__main__':
app.run(port=5555, debug=True)