Building RESTful APIs with Flask in Python

This article demonstrates how to quickly build a RESTful API in Python using the Flask library. To know about RESTful APIs, read the article on Design principles behind RESTful APIs.

Boiler plate

There is very little boiler plate necessary when defining a flask app. Something as limited as:

from flask import Flask
from datetime

# define a variable to hold you app
app = Flask(__name__)

# define your resource endpoints
app.route('/')
def index_page():
    return "Hello world"

app.route('/time', methods=['GET'])
def get_time():
    return str(datetime.datetime.now())

# server the app when this file is run
if __name__ == '__main__':
    app.run()

In the code above, you have defined 2 endpoints - a root / landing page and a /time endpoint. You can run this app from terminal as python -m <filename> which will start the webserver and give you an IP address to go or call.

Passing arguments

When calling an endpoint, users can send the server some information. This info can be sent via GET or POST calls. By following HTTP verbs, if user wants to collect info back, they should send it via GET, else if the call is to perform some operation on the server side, then use POST, PUT, DELETE.

from flask import request # used to parse payload
app.route('/hello')
def welcome_message():
    """
    Called as /hello?name='value'
    """
    # if user sends payload to variable name, get it. Else empty string
    name = request.get('name', '') 
    return f'Welcome {name}'

Note, sometimes your web app needs to make calls to other resources on the web. For this you can use the requests library. The request module of Flask used in the snippet is not to be confused with the requests library.

Rendering HTML pages

Flask allows you to define a templates directory and put HTML pages into it. In these HTML pages, you can define variables and pass values to be displayed when rendering the HTML pages. This totally expands your possibilities without having to write a line of JS code.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Thermos - building webapis with flask</title>
</head>
<body>
<p>
    A little bit about the machine running this site:

    <h4>OS type: {{os_type}}; OS name: {{os_name}}</h4>
</p>
</body>
</html>

Put the above HTML page (saved as index.html) into a templates directory. Then you can render this page as

from flask import render_template
app.route('/welcomePage')
def welcome_page():

    return render_template('index.html',
                            'os_type' = sys.platform,
                            'os_name' = os.name)

Getting user input via HTML pages

Not only are HTML pages good to display content on a UI, they are good to collect user input for processing. The snippet below defines a resource that when called via GET, will show an HTML page with form controls. If accessed via POST, it performs the actual processing defined by the resource.

@app.route('/eyeFromAbove', methods=['GET', 'POST'])
def eye_from_above():
    """
    If GET request - Displays a html page with box to enter address
    if POST request - renders output html with image from satellite
    :return:
    """
    if request.method == 'POST':
        # User could have used the web UI or called the endpoint headless

        # If user used the web UI, then address comes as form data
        if request.form is not None:
            address = request.form.get('address', None)
            date = request.form.get('date', None)
        else:
            # request data comes via args
            address = request.args.get('address', None)
            date = request.args.get('date', None)
        if address:
            geocode_dict = geocode_address_executor(address)
            lon = geocode_dict['location']['x']
            lat = geocode_dict['location']['y']

        else: # when no address is loaded
            flash('No address specified')
            return redirect(request.url)

        base_url = 'https://api.nasa.gov/planetary/earth/imagery'
        # for some reason, NASA server rejects the GET request if I send data over payload
        if date:
            full_url = f'{base_url}/?lat={lat}&lon={lon}&date={date}&cloud_score=False&api_key={key}'
        else:
            full_url = f'{base_url}/?lat={lat}&lon={lon}&cloud_score=False&api_key={key}'


        # construct the query and get download url
        # resp = requests.get(base_url, params)
        resp = requests.get(full_url)

        if resp.status_code == 200:
            resp_dict = json.loads(resp.text)
        else:
            return json.dumps({'error':resp.text})

        # Download the image from Google Earth Engine API.
        img_resp = requests.get(resp_dict['url'])
        if img_resp.status_code == 200:
            img_filename = address.replace(' ','_').replace('-','').replace('.','').replace('*','').replace(',','')
            with open(f'eye_in_sky_queries/{img_filename}.png', 'wb') as img_handle:
                img_handle.write(img_resp.content)
        else:
            return json.dumps({'error':img_resp.text})

        # render the HTML page
        return render_template('eye_from_above.html',
                               media_type='image',
                               media_url = os.path.join('/','eye_in_sky_queries',img_filename+'.png'),
                               img_date = resp_dict['date'],
                               img_id = resp_dict['id'],
                               img_dataset = resp_dict['resource']['dataset'])

    elif request.method == 'GET':
        # case when page is loaded on browser
        return '''
            <!doctype html>
            <title>Enter address to view</title>
            <h1>Enter the address to get image of</h1>
            <form method=post enctype=multipart/form-data>
              <input type=text name=address value=address>
              <input type=text name=date value='2013-12-17'>
              <input type=submit value=Submit>
            </form>
            '''

# this resource is needed to render images from disk. Needed for the template HTML pages
@app.route('/eye_in_sky_queries/<filename>')
def uploaded_file(filename):
    return send_from_directory('eye_in_sky_queries',
                               filename)

Now, to display the image, we need this part in the template HTML file

<body>
    {% if media_type == 'image' %}
        <img src={{media_url}} class="img-fluid" alt="Responsive image">
</body>

Database CRUD operations

One of the popular use cases of RESTful web services is, to perform operations on backend database via the web. In the snippet below, I am using sqlalchemy library to create an in-memory sqlite database and perform CRUD - Create, Read, Update, Delete operations on it.

sqlalchemy in 2 minutes

Sqlalchemy library is an ORM (Object Relatinal Mapper) which allows representing database elements as Python objects. In addition, it allows you to connect to a DB and perform CRUD ops in addition to others.

Connecting to a DB. In this case a sqllite db is created. If you want the db in-memory, then specify memory as location.

from sqlalchemy import create_engine
engine = create_engine('sqlite:///filename.db')

The next step is to establish a session to this newly created db.

from sqlalchemy.orm import sessionmaker
DBSession = sessionmaker(bind=engine)
my_session = DBSession()

The next step is a little boiler plate and can be seen in many examples using sqlalchemy.

from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()

Next step is to create a Model - this represents the table, columns, rows as Python objects.

from sqlalchemy import column, Integer, Numeric, String

class Puppy(Base): #must inherit from declarative base
    __tablename__ = 'puppy'

    puppy_id = Column(Integer, primary_key=True)
    puppy_name = Column(String[30])
    # ... other columns

Finnaly, we need to create the table define above on the db.

Base.metadata.create_all(engine)

Inserting rows

Create an instance of the class you created that inherited the declarative_base.

puppy1 = Puppy(puppy_name = 'nemo') #pass the columns info to the constructor

# insert row
session.add(puppy1)
session.commit()

Since, we did not pass puppy_id, the Primary key, the session knows this is a new row and it has to be created. Else, to edit an existing row, you still do an add but have the primary key specified.

User facing CRUD API:

Below is an example endpoint that can perform all 4 CRUD operations:

@app.route('/addresses/<int:id>', methods=['GET', 'PUT', 'DELETE'])
def address_id_handler(id):
    """
    GET - called as /addresses/25
    PUT - called to update as /addresses/25?address='abc'&lat=25&lon=89
    DELETE - called as /addresses/25
    :param id:
    :return:
    """
    if request.method == 'GET':
        return jsonify(read_address(session, address_id=id))

    elif request.method == 'PUT':
        address = request.form.get('address','dummy')
        lat = request.form.get('lat',0.1)
        lon = request.form.get('lon',0.1)
        update_address(session, address_id=id, search_string=address, lat=lat, lon=lon)
        return jsonify({'success': True})

    elif request.method == 'DELETE':
        delete_address(session, id)

If you called /addresses/<id> with an existing id via GET, you perform a Read operation. Calling with a new id via PUT will do a Create operation. However, calling with an existing id via PUT will do an Update operation. Finally, DELETE will Delete that record from the DB.

Conclusion

To view the full app in action, checkout the apps section section. To see its source code, see this GitHub repo.