Home Articles Categories Series
Pythonise Just now

Flask HTTP methods | Learning Flask Ep. 19

Understanding when and where to use GET, POST, PUT, PATCH, DELETE and a brief introduction to HTTP methods in Flask


Article Posted on by in Flask
Julian Nash 9 months ago in Flask

Flask supports the common HTTP methods, including GET, POST, PUT, PATCH, DELETE and working with them is extremely simple, allowing us to build URL's and endpoints which only listen for certain HTTP methods.

In this part of the "Learning Flask" series, we're going to build a simple application to demonstrate working with the 5 HTTP methods listed above, along with examples of when and how to use them.

If you're new to programming or new to working on web, getting to know the HTTP methods and understanding the basics of HTTP is extremely valuable, so I'd suggest reading up on it after you're done here.

This guide isn't designed to be an in depth tutorial on HTTP, however it should give you enough information to use them in your Flask applications and hopefully you'll learn something new!

HTTP basics

The "Hyper text transfer protocol" AKA HTTP is the world wide protocol for how we communicate and send/receive information on the web.

It works on a "Request/Response" protocol, where a client makes a request to a web server which then returns a response.

For example, when you clicked on this very URL, your browser (The client) made a request to the Pythonise web server, which internally triggered a series of events to return a response, which is the HTML you're reading now.

Like I said, this is a very basic introduction to HTTP and we're not going to go into too much detail on it in this guide. For more information, the Wikipedia page is a good place to start.

HTTP status codes

When something goes well, it's nice to get a pat on the back, likewise when something doesn't go as planned, it helps to know why so you can put it right.

Feedback is very important and it's no different on the web!

HTTP status codes are used as a feedback system, issued by the server in response to a request from the client to indicate the status of the request.

If the request went well, the server returns a status code to the client to indicate that everything is OK, likewise a bad request or error will result in a different code to tell the client that something didn't go as expected.

If you've spend any time on the web, you've most likely come across these status codes yourself.

Ever clicked on a link and got a 404 NOT FOUND? That's the web servers way of saying "I got your message, but whatever you're looking for, I don't have it". However rather than saying that it simply returns a 404 status code (Which is much more friendly for machines to understand 馃)

HTTP status codes are made up of 3 digits that fall into 5 categories, with each category representing a certain class of code.

The first digit is the category and the 5 categories correspond to the following class:

  • 1xx - Informational
  • 2xx - Success
  • 3xx - Redirection
  • 4xx - Client errors
  • 5xx - Server errors

The example of the 404 status code falls under the "client error" category, where the client tried to request something that doesn't exist on the server.

The last 2 digits of the code don't fall under any kind of class or category, but are used to provide more information and context.

Again to use the 404 example, the last 2 digits refer to NOT FOUND, giving more context to the type of client error.

A full list of HTTP status codes can be found here at the Wiki.

HTTP methods

The method is the type of action you want the request to perform and is sent from the client to the server on every request.

There are several HTTP methods but we're only going to cover 5 in this article:

  • GET - Used to fetch the specified resource
  • POST - Used to create new data at the specified resource
  • PUT - Used to create new data or replace existing data at the specified resource
  • PATCH - Used to create new data or update/modify existing data at the specified resource
  • DELETE - Used to delele existing data at the specified resource

Requesting a URL is an example of a GET request, where your browser makes a request for resources at a specified location (the URL) and the server returns some HTML. GET requests are "safe" as they aren't able to modify state or data on the server.

An example use case of a POST request would be creating a new account on a website or application, whereby the resource doesn't already exist.

Flask HTTP methods

By default, routes created with @app.route(/example) only listen and respond to GET requests and have to be instructed to listen and respond to other methods using the methods keyword & passing it a list of request methods.

Let's explore some common use cases for GET requests.

GET requests

Ubiquitously used in Flask applications, the GET method is used to return data at a specified resource/location.

Returning text

Possible one of the most simple routes you can write in a flask app simply returns a string:

@app.route("/get-text")
def get_text():
    return "some text"
Rendering templates

A very common use case for a GET request is to return some HTML:

from flask import render template

@app.route("/")
def index():
    return render_template("index.html")
Handlinq query strings

No different from either of the 2 previous examples, with the addition of handling a query string in the URL and returning a formatted string to the client:

from flask import request

@app.route("/qs")
def qs():

    if request.args:
        req = request.args
        return " ".join(f"{k}: {v} " for k, v in req.items())

    return "No query"

Requesting /qs?name=john&language=python returns the string name: john language: python.

Fetching resources

Again, not really any different from the previous examples, just in this case fetching and returning a resource as a JSON string. We've also created a mock database called stock:

from flask import make_response, jsonify

stock = {
    "fruit": {
        "apple": 30,
        "banana": 45,
        "cherry": 1000
    }
}

@app.route("/stock")
def get_stock():

    res = make_response(jsonify(stock), 200)

    return res

Extending the previous example with some URL variables, ding a lookup and returning a JSON response (Note the use of the 404 if the collection or member is not found):

@app.route("/stock/<collection>")
def get_collection(collection):

    """ Returns a collection from stock """

    if collection in stock:
        res = make_response(jsonify(stock[collection]), 200)
        return res

    res = res = make_response(jsonify({"error": "Not found"}), 404)

    return res


@app.route("/stock/<collection>/<member>")
def get_member(collection, member):

    """ Returns the qty of the collection member """

    if collection in stock:
        member = stock[collection].get(member)
        if member:
            res = make_response(jsonify(member), 200)
            return res

        res = make_response(jsonify({"error": "Not found"}), 404)
        return res

    res = res = make_response(jsonify({"error": "Not found"}), 404)
    return res

In summary, use GET requests when you just need to return resources to the client and NOT make any changes to the state/data of your application.

GET and POST

In many cases such as rendering forms, you'll need a route to handle more than just GET requests.

Making any request other than GET to a route without the methods argument and a list of methods will result in a 405 METHOD NOT ALLOWED HTTP status code, as methods must be declared for the route to respond to.

Adding multiple request methods to a route is done with the following:

@app.route("/example", methods=["METHOD_A", "METHOD_B"])  # GET POST PUT PATCH DELETE etc..

The route will now listen and respond to both methods provided which means we have to put in some control flow to handle each type of request.

Fortunately this is made easy using the request object, in particular the request.method attribute which returns the method for the current request.

This verbose and silly example illustrates the logic, assuming we want to listen for GET and POST requests:

@app.route("/log-in", methods=["GET", "POST"])
def log_in():

    if request.method == "POST":
        # Attempt the login & do something else
    elif request.method == "GET":
        return render_template("log_in.html")

As Flask routes default to GET requests, we can remove the elif statement and let Flask return the template:

@app.route("/log-in", methods=["GET", "POST"])
def log_in():

    if request.method == "POST":
        # Only if the request method is POST
        # attempt the login & do something else

    # Otherwise default to this
    return render_template("log_in.html")

A working example can be seen below where we're rendering a template which a user can then use to add a new collection to our stock database:

@app.route("/add-collection", methods=["GET", "POST"])
def add_collection():

    """ 
    Renders a template if request method is GET.
    Creates a collection if request method is POST
    and if collection doesn't exist
    """

    if request.method == "POST":

        req = request.form

        collection = req.get("collection")
        member = req.get("member")
        qty = req.get("qty")

        if collection in stock:
            message = "Collection already exists"
            return render_template("add_collection.html", stock=stock, message=message)

        stock[collection] = {member: qty}
        message = "Collection created"

        return render_template("add_collection.html", stock=stock, message=message)

    return render_template("add_collection.html", stock=stock)

POST requests

Flask routes listen for GET requests by default, so we must implicitly instruct them to listen for anything other than GET.

In the example below, we've passed methods=["POST"] to the @app.route() decorator, meaning this route will ONLY respond to POST requests:

@app.route("/stock/<collection>", methods=["POST"])
def create_collection(collection):

    """ Creates a new collection if it doesn't exist """

    req = request.get_json()

    if collection in stock:
        res = make_response(jsonify({"error": "Collection already exists"}), 400)
        return res

    stock.update({collection: req})

    res = make_response(jsonify({"message": "Collection created"}), 201)
    return res

We're checking to see if the collection variable (passed in via the URL) is in our stock database and if not, create it, otherwise return a 400 BAD REQUEST to indicate the resource already exists.

POST requests should be used to create NEW resources (New users, devices, posts, articles, datasets etc..)

Again, if you tried to access this resource from the browser, you'd be greeted with a 405 METHOD NOT ALLOWED status as we've not provided GET as one of the available request methods to listen for.

PUT requests

PUT requests are similar to POST requests but serve a very different purpose.

As we explained earlier, PUT should be used to create or replace a resource, meaning if it doesn't exist - create it, however if it does exist, replace it.

Check out the example below:

@app.route("/stock/<collection>", methods=["PUT"])
def put_collection(collection):

    """ Replaces or creates a collection """

    req = request.get_json()

    if collection in stock:
        stock[collection] = req
        res = make_response(jsonify({"message": "Collection replaced"}), 200)
        return res

    stock[collection] = req
    res = make_response(jsonify({"message": "Collection created"}), 201)
    return res

Since PUT requests shouldn't care about the existing data or resource, we've decided to go ahead and create the resource with no regard for any existing data. The only dirrerence in the logic is the HTTP status code.

If the collection DIDN'T exist, we're returning a 201 CREATED status. Whereas if the collection DID exist, we're returning a 200 OK to indicate that the collection has been replaced.

It's probably bad design to replace an entire collection using the PUT request, but for demonstrational purposes it'll do. A better solution would be to use PUT to replace or create a value for one of the members in the collection.

PATCH requests

We're using a PATCH request to update OR create a resource in our stock database:

@app.route("/stock/<collection>", methods=["PATCH"])
def patch_collection(collection):

    """ Updates or creates a collection """

    req = request.get_json()

    if collection in stock:
        for k, v in req.items():
            stock[collection][k] = v

        res = make_response(jsonify({"message": "Collection updated"}), 200)
        return res

    stock[collection] = req

    res = make_response(jsonify({"message": "Collection created"}), 201)
    return res

Rather than replace the collection entirely, we're iterating over the keys and values in the request body and updating the values in the collection, only creating new members if they don't exist.

And just like in PUT, we're returning a 200 if the collection was updated and a 201 if the collection was created.

Let's cover the last request method in this article, DELETE.

Delete requests

Just like it says on the tin, DELETE requests should be used to delete a resource.

As per the rest of the examples, we provide methods=["DELETE"] in the @app.route() decorator, meaning this route will only listen for that specific method.

In the 2 examples below, you'll see we're deleting a collection and deleting individual members from a collection with 2 separate routes:

@app.route("/stock/<collection>", methods=["DELETE"])
def delete_collection(collection):

    """ If the collection exists, delete it """

    if collection in stock:
        del stock[collection]
        res = make_response(jsonify({}), 204)
        return res

    res = make_response(jsonify({"error": "Collection not found"}), 404)
    return res

@app.route("/stock/<collection>/<member>", methods=["DELETE"])
def delete_member(collection, member):

    """ If the collection exists and the member exists, delete it """

    if collection in stock:
        if member in stock[collection]:
            del stock[collection][member]
            res = make_response(jsonify({}), 204)
            return res

        res = make_response(jsonify({"error": "Member not found"}), 404)
        return res

    res = make_response(jsonify({"error": "Collection not found"}), 404)
    return res

On deletion, we're returning a 204 NO CONTENT status code to indicate a successful transaction and we have no content to return.

If the resource isn't found, I/e the collection or member doesn't exist, we're returning a 404 NOT FOUND.

Wrapping up

The code snippets shown here were designed to demonstrate how to work with some of the common request methods in Flask, rather than be examples of how to write a REST API, so take the examples with a pinch of salt.

restfulapi.net is a great place to learn more about API design and using the various request methods, along with the 2 Wikipedia links below.

I hope you learned something new!

Last modified 28 Feb 2019
Did you find this article useful?
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License
Contents
Loading...