Single Page Apps with Vue.js and Flask: RESTful API with Flask

RESTful API with Flask

Welcome to the fourth post on using Vue.js and Flask for full-stack web development. The focus of this post will be on building a backend REST API using the Python based Flask web framework.

The code for this post is in a repo on my GitHub account under the branch FourthPost.

Series Content

  1. Seup and Getting to Know VueJS
  2. Navigating Vue Router
  3. State Management with Vuex
  4. RESTful API with Flask (you are here)
  5. AJAX Integration with REST API
  6. JWT Authentication
  7. Deployment to a Virtual Private Server

Brief Explanation of Flask

Flask is a Python-based micro-framework for rapid prototyping and development of small to moderate size web applications. Flask has already been covered in a couple of prior posts here and here on StackAbuse so, I will not be going into great detail pertaining to the basic or common parts of Flask. Instead I will be taking a more pragmatic approach focusing mostly on constructing a RESTful API to fuel the frontend with data, which I covered in the articles leading up to this one.

Scaffolding out the Backend Project Files

I begin in the /backend directory by creating a Python3 virtual environment and installing Flask and a few other necessary libraries.

$ python -m venv venv
$ source venv/bin/activate
(venv) $ pip install Flask Flask-SQLAlchemy Flask-Migrate Flask-Script requests

One thing that makes Flask (and in large part the entire Python ecosystem) so awesome are the large number of well-designed packages available on PyPI. Below is a brief explanation of the libraries that I installed and their intended usage.

  • Flask: Web micro framework
  • Flask-SQLAlchemy: SQLAlchemy-based ORM with some Flask-specific awesome sauce packaged with it
  • Flask-Migrate: Database migration library
  • Flask-Script: Extremely useful package for interacting with a Flask application from the command line
  • requests: a handy package for making network requests which I will use to test the REST API

In the /backend directory I make a few new files called manage.py and appserver.py. Also, I will make a new directory inside of /backend that will become my "surveyapi" Flask application. Within the surveyapi directory I make the files __init__.py, models.py, application.py, and api.py. This results in a directory structure beginning at /backend like so (omitting the venv directory).

├── manage.py
├── appserver.py
└── surveyapi
    ├── __init__.py
    ├── api.py
    ├── application.py
    ├── config.py
    └── models.py

Below is a brief description of what each file is going to be used for:

  • manage.py: access to the Flask application instance for various Flask-Script commands
  • appserver.py: start-up script for running the surveyapi application
  • surveyapi/: the backend Flask application
  • __init__.py: turns the surveyapi directory into a valid Python package
  • api.py: for defining REST API route endpoints capable of consuming and producing JSON request and responses
  • application.py: for creating an instance of the Flask application
  • config.py: contains configuration settings for the Flask application
  • models.py: for defining classes that will serve as data objects for the survey application such as Survey, Question, and Choice

Creating an Application Factory

I will begin coding the surveyapi application by defining some settings inside of config.py like so:

"""
config.py  
- settings for the flask application object
"""

class BaseConfig(object):  
    DEBUG = True
    SQLALCHEMY_DATABASE_URI = 'sqlite:///survey.db'
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    # used for encryption and session management
    SECRET_KEY = 'mysecretkey'

This config class defines a SQLALCHEMY_DATABASE_URI application database connection URI to a single file SQLite database called survey.db. It also provides SECRET_KEY config option that is used for encryption.

Inside of application.py I will create what is known as an application factory function, which does exactly what it sounds like, it creates a Flask application instance. In addition to instantiating an instance of Flask it also sources the BaseConfig object and registers the API routes blueprint I will make next.

"""
application.py  
- creates a Flask app instance and registers the database object
"""

from flask import Flask

def create_app(app_name='SURVEY_API'):  
    app = Flask(app_name)
    app.config.from_object('surveyapi.config.BaseConfig')
    from surveyapi.api import api
    app.register_blueprint(api, url_prefix="/api")
    return app

Blueprint API

Next I will move into the api.py module where I can define a Blueprint object called api containing RESTful routes. To keep things simple I will begin by just defining a simple view function called say_hello() associated with the endpoint /api/hello/<string:name>/. The <string:name> portion of the url is a dynamic string variable that gets passed to the view function say_hello(name) as a function parameter which I use in the JSON response message that gets returned.

"""
api.py  
- provides the API endpoints for consuming and producing
  REST requests and responses
"""

from flask import Blueprint, jsonify, request

api = Blueprint('api', __name__)

@api.route('/hello/<string:name>/')
def say_hello(name):  
    response = { 'msg': "Hello {}".format(name) }
    return jsonify(response)

Dev Server Entry Point and Validating the Setup

To test this out I need to add a couple of lines of code in appserver.py to create an instance of the app. This enables me to fire up the Flask dev server by calling the run() method on the app instance.

"""
appserver.py  
- creates an application instance and runs the dev server
"""

if __name__ == '__main__':  
    from surveyapi.application import create_app
    app = create_app()
    app.run()

To run the Flask dev server all I need to do is launch the Python interpreter and feed it the appserver.py script as shown below.

(venv) $ python appserver.py
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 676-284-544

Now to test the new endpoint, in a new terminal with the virtual environment activated, I will fire up a Python interpreter and make a GET request to http://localhost:5000/api/hello/adam/ using the requests package.

(venv) $ python
>>> import requests
>>> response = requests.get('http://localhost:5000/api/hello/adam/')
>>> print(response.json())
{'msg': 'Hello adam'}

Defining the Data Layer

Now that I have verified that I have a functioning Flask application scaffolded out I can focus on building out the data layer with the help of the Flask-SQLAlchemy ORM. Implementing a data layer will require writing some data classes inside of models.py such as:

  • Survey: this is the top level object that will contain one or more questions along with their choices
  • Question: objects that belong to a survey object and contain choices
  • Choice: objects that belong to a question and represent choices for the survey's question

These data classes will poses fields that in large part will mimic the ones previously described in the articles on building the Vue.js frontend application, but these will map to database tables where their data will be persisted.

"""
models.py  
- Data classes for the surveyapi application
"""

from datetime import datetime  
from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()

class Survey(db.Model):  
    __tablename__ = 'surveys'

    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.Text)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    questions = db.relationship('Question', backref="survey", lazy=False)

    def to_dict(self):
        return dict(id=self.id,
                    name=self.name,
                    created_at=self.created_at.strftime('%Y-%m-%d %H:%M:%S'),
                    questions=[question.to_dict() for question in self.questions])

class Question(db.Model):  
    __tablename__ = 'questions'

    id = db.Column(db.Integer, primary_key=True)
    text = db.Column(db.String(500), nullable=False)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    survey_id = db.Column(db.Integer, db.ForeignKey('surveys.id'))
    choices = db.relationship('Choice', backref='question', lazy=False)

    def to_dict(self):
        return dict(id=self.id,
                    text=self.text,
                    created_at=self.created_at.strftime('%Y-%m-%d %H:%M:%S'),
                    survey_id=self.survey_id,
                    choices=[choice.to_dict() for choice in self.choices])

class Choice(db.Model):  
    __tablename__ = 'choices'

    id = db.Column(db.Integer, primary_key=True)
    text = db.Column(db.String(100), nullable=False)
    selected = db.Column(db.Integer, default=0)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    question_id = db.Column(db.Integer, db.ForeignKey('questions.id'))

    def to_dict(self):
        return dict(id=self.id,
                    text=self.text,
                    created_at=self.created_at.strftime('%Y-%m-%d %H:%M:%S'),
                    question_id=self.question_id)

As mentioned previously, I am using the Flask-specific extension of SQLAlchemy called Flask-SQLAlchemy to power the ORM for this application. I like Flask-SQLAlchemy because it has a fairly Pythonic API and it provides sensible defaults for defining and working with data classes.

Each class inherits from the SQLAlchemy's Model base class which provides intuitive and readable utility methods for interacting with the data stored in the database. Furthermore, each class is comprised of a series of class fields that are translated into database table fields as specified by the SQLAlchemy Column class and associated type (ie, Integer, String, DateTime, Text, ...).

You will also notice that each class has a common to_dict() method. This method will come in handy for serializing the models' data into JSON when sending it over the wire to the frontend client.

Next up on the list to do is to register the SQLAlchemy object, db, with the Flask application object in application.py.

"""
application.py  
- creates a Flask app instance and registers the database object
"""

from flask import Flask

def create_app(app_name='SURVEY_API'):  
    app = Flask(app_name)
    app.config.from_object('surveyapi.config.BaseConfig')

    from surveyapi.api import api
    app.register_blueprint(api, url_prefix="/api")

    from surveyapi.models import db
    db.init_app(app)

    return app

The final thing I would like to do is to bring together the Flask-Script and Flask-Migrate extension packages inside the manage.py module to enable migrations. This handy module, manage.py, will pull together the data classes I just defined and link them to the application context along with the Flask-Migrate and Flask-Script machinery.

"""
manage.py  
- provides a command line utility for interacting with the
  application to perform interactive debugging and setup
"""

from flask_script import Manager  
from flask_migrate import Migrate, MigrateCommand

from surveyapi.application import create_app  
from surveyapi.models import db, Survey, Question, Choice

app = create_app()

migrate = Migrate(app, db)  
manager = Manager(app)

# provide a migration utility command
manager.add_command('db', MigrateCommand)

# enable python shell with application context
@manager.shell
def shell_ctx():  
    return dict(app=app,
                db=db,
                Survey=Survey,
                Question=Question,
                Choice=Choice)

if __name__ == '__main__':  
    manager.run()

I am accomplishing two things in this bit of code above. First, I am creating an instance of the Flask application object so it provides context to the Migrate(app, db) and Manage(app) instances. Then I am adding a command to the manager object that allows me to create and run migrations from the command line like so:

(venv) $ python manage.py db init
  • Initialize the migrations directory next to the surveyapi application and database file survey.db
(venv) $ python manage.py db migrate
  • Create an initial migration file to translate the classes in models.py to SQL that will generate corresponding tables
(venv) $ python manage.py db upgrade
  • Run the migration to upgrade the database with the tables described in the prior step

The last thing that I am doing in the manage.py module is creating another custom command utilizing @manager.shell to decorate a shell_ctx() function which returns a dict mapping keywords to the app and db objects along with the Survey, Question and, Choice data classes.

I will now take advantage of the usefulness of this shell utility command to demonstrate how to work with the Flask-SQLAlchemy ORM within the python interpreter it produces.

(venv) $ python manage.py shell
(venv) Adams-MacBook-Pro:backend adammcquistan$ python manage.py shell
>>> survey = Survey(name='Dogs')
>>> question = Question(text='What is your favorite dog?')
>>> question.choices = [Choice(text='Beagle'), Choice(text='Rottweiler'), Choice(text='Labrador')]
>>> question2 = Question(text='What is your second favorite dog?')
>>> question2.choices = [Choice(text='Beagle'), Choice(text='Rottweiler'), Choice(text='Labrador')]
>>> survey.questions = [question, question2]
>>> db.session.add(survey)
>>> db.session.commit()
>>> surveys = Survey.query.all()
>>> for s in surveys:
...     print('Survey(id={}, name={})'.format(s.id, s.name))
...     for q in s.questions:
...             print('  Question(id={}, text={})'.format(q.id, q.text))
...             for c in q.choices:
...                     print('    Choice(id={}, text={})'.format(c.id, c.text))
...
Survey(id=1, name=Dogs)  
  Question(id=1, text=What is your favorite dog?)
    Choice(id=1, text=Beagle)
    Choice(id=3, text=Labrador)
    Choice(id=2, text=Rottweiler)
  Question(id=2, text=What is your second favorite dog?)
    Choice(id=4, text=Beagle)
    Choice(id=6, text=Labrador)
    Choice(id=5, text=Rottweiler)

Thats pretty slick, right?

I'm not just talking about the elegant and readable syntax of the ORM, but the incredibly empowering ability to fire up a Python interpreter containing the application context to do quick little experiments with the models in your application. I cannot tell you how much of a productivity boost this has provided me when building out backend applications, and I seriously suggest you utilize it when doing the same.

Completing the RESTful API

Now that the data access layer is built I can focus my attention on completing the implementation necessary for the RESTful API. This is going to handle consuming and returning the application resources like the Survey, Question and Choice data. The use cases required of the RESTful API include the following:

  • Fetch all surveys along with their questions and choices
  • Fetch a single survey along with its questions and choices
  • Create a new survey along with its specified questions and choices
  • Update a survey's response choices once a survey has been taken

To start with I will go ahead and import all of the data classes along with the SQLAlchemy db instance so I have access to them. At the top of api.py I add the following imports:

"""
api.py  
- provides the API endpoints for consuming and producing
  REST requests and responses
"""

from flask import Blueprint, jsonify, request  
from .models import db, Survey, Question, Choice  

As for the actual resource endpoints, I will start by coding up the ability to fetch all survey resources. Inside api.py I need to replace the /hello/<string:name>/ endpoint with the route /surveys/ endpoint and surveys() view function.

@api.route('/surveys/')
def surveys():  
    surveys = Survey.query.all()
    return jsonify({ 'surveys': [s.to_dict() for s in surveys] })

If the dev server is still running then once I save the project files the server should automatically reload refreshing all the changes. If not, then running (venv) $ python appserver.py will start the server. Now in another terminal with the virtual environment activated I can use the requests package to test this new endpoint. However, I would like to share a pro-tip on displaying JSON responses in a more readable way by using another awesome Python package called pprint.

(venv) $ pip install pprint
(venv) $ python
>>> import pprint, requests
>>> pp == pprint.PrettyPrinter()
>>> resp = requests.get('http://localhost:5000/api/surveys/')
>>> pp.pprint(resp.json())
{'surveys': [{
     'created_at': '2018-03-06 03:52:44',
     'id': 1,
     'name': 'Dogs',
     'questions': [{
          'choices': [{
               'created_at': '2018-03-06 03:52:44',
               'id': 1,
               'question_id': 1,
               'text': 'Beagle'
              },{
               'created_at': '2018-03-06 03:52:44',
               'id': 3,
               'question_id': 1,
               'text': 'Labrador'
              },{
               'created_at': '2018-03-06 03:52:44',
               'id': 2,
               'question_id': 1,
               'text': 'Rottweiler'}],
            'created_at': '2018-03-06 03:52:44',
            'id': 1,
            'survey_id': 1,
            'text': 'What is your favorite dog?'
         },{
          'choices': [{
              'created_at': '2018-03-06 03:52:44',
              'id': 4,
              'question_id': 2,
              'text': 'Beagle'
             },{
              'created_at': '2018-03-06 03:52:44',
              'id': 6,
              'question_id': 2,
              'text': 'Labrador'
             },{
              'created_at': '2018-03-06 03:52:44',
              'id': 5,
              'question_id': 2,
              'text': 'Rottweiler'}],
          'created_at': '2018-03-06 03:52:44',
          'id': 2,
          'survey_id': 1,
          'text': 'What is your second favorite dog?'}]}
    ]}

Next up I will implement the functionality to fetch a single survey by its id with the URL endpoint /surveys/id/ and view function survey(id). Immediately following the surveys() API view function I place the following code:

@api.route('/surveys/<int:id>/')
def survey(id):  
    survey = Survey.query.get(id)
    return jsonify({ 'survey': survey.to_dict() })

Again, I will save the files and test the new API endpoint to make sure it serves up a valid response.

>>> resp = requests.get('http://localhost:5000/api/surveys/1/')
>>> pp.pprint(resp.json())
{'survey': {
     'created_at': '2018-03-06 03:52:44',
     'id': 1,
     'name': 'Dogs',
     'questions': [{
          'choices': [{
               'created_at': '2018-03-06 03:52:44',
               'id': 1,
               'question_id': 1,
               'text': 'Beagle'
              },{
               'created_at': '2018-03-06 03:52:44',
               'id': 3,
               'question_id': 1,
               'text': 'Labrador'
              },{
               'created_at': '2018-03-06 03:52:44',
               'id': 2,
               'question_id': 1,
               'text': 'Rottweiler'}],
            'created_at': '2018-03-06 03:52:44',
            'id': 1,
            'survey_id': 1,
            'text': 'What is your favorite dog?'
         },{
          'choices': [{
              'created_at': '2018-03-06 03:52:44',
              'id': 4,
              'question_id': 2,
              'text': 'Beagle'
             },{
              'created_at': '2018-03-06 03:52:44',
              'id': 6,
              'question_id': 2,
              'text': 'Labrador'
             },{
              'created_at': '2018-03-06 03:52:44',
              'id': 5,
              'question_id': 2,
              'text': 'Rottweiler'}],
          'created_at': '2018-03-06 03:52:44',
          'id': 2,
          'survey_id': 1,
          'text': 'What is your second favorite dog?'}]}
    }

So far I have only used the default HTTP GET route method suitable for fetching data from RESTful APIs. However, for the last two pieces of functionality I will need to utilize the HTTP POST and PUT methods for the endpoints /api/surveys/ and /api/surveys/id/, respectively. I will use the HTTP POST method for creating new surveys and the HTTP PUT method for updating an existing survey with a new set of selected response choices.

For the /api/surveys/ route I will need to add a method parameter to the route declaration to specify that it accepts both GET and POST methods, methods=('GET','POST'). In addition I will modify the body of the surveys() view function to differentiate the method type and add the ability to save a new survey to the database.

@api.route('/surveys/', methods=('GET', 'POST'))
def fetch_surveys():  
    if request.method == 'GET':
        surveys = Survey.query.all()
        return jsonify({ 'surveys': [s.to_dict() for s in surveys] })
    elif request.method == 'POST':
        data = request.get_json()
        survey = Survey(name=data['name'])
        questions = []
        for q in data['questions']:
            question = Question(text=q['text'])
            question.choices = [Choice(text=c['text'])
                                for c in q['choices']]
            questions.append(question)
        survey.questions = questions
        db.session.add(survey)
        db.session.commit()
        return jsonify(survey.to_dict()), 201

Again, I will want to save the project and test this to make sure I have a fully functional survey saving resource.

>>> import json
>>> survey = {
...   'name': 'Cars',
...   'questions': [{
...     'text': 'What is your favorite car?',
...     'choices': [
...       { 'text': 'Corvette' },
...       { 'text': 'Mustang' },
...       { 'text': 'Camaro' }]
...   }, {
...     'text': 'What is your second favorite car?',
...     'choices': [
...       { 'text': 'Corvette' },
...       { 'text': 'Mustang' },
...       { 'text': 'Camaro' }]
...   }]
... }
>>> headers = {'Content-type': 'application/json'}
>>> resp = requests.post('http://localhost:5000/api/surveys/', headers=headers, data=json.dumps(survey))
>>> resp.status_code
201  

The final piece to implement is the ability to update an existing survey with new survey response selections. Again, I will need to add the methods of GET and PUT to the /api/surveys/id/ route definition, methods=('GET', 'PUT'). Then I update the survey(id) view function to update the associated survey's question choices specified as being selected in the JSON body of the PUT request.

@api.route('/surveys/<int:id>/', methods=('GET', 'PUT'))
def survey(id):  
    if request.method == 'GET':
        survey = Survey.query.get(id)
        return jsonify({ 'survey': survey.to_dict() })
    elif request.method == 'PUT':
        data = request.get_json()
        for q in data['questions']:
            choice = Choice.query.get(q['choice'])
            choice.selected = choice.selected + 1
        db.session.commit()
        survey = Survey.query.get(data['id'])
        return jsonify(survey.to_dict()), 201

Lastly, I need to save all my files and do one final test like so:

>>> survey_choices = {
...   'id': 1,
...   'name': 'Dogs',
...   'questions': [
...     { 'id': 1, 'choice': 1 },
...     { 'id': 2, 'choice': 5 }]
... }
>>> headers = {'Content-type': 'application/json'}
>>> resp = requests.put('http://localhost:5000/api/surveys/1/', data=json.dumps(survey_choices), headers=headers)
>>> resp.status_code()
201  

Resources

Want to learn more about Python and building backend APIs? Try checking out a course like REST APIs with Flask and Python for a deeper dive in to backend web development with Python.

Conclusion

In this article I have covered how to implement a simple, rather bare-bones, RESTful API using Flask according to the following table:

Route Method Functionaility
/api/surveys/ GET Retrieve all surveys
/api/surveys/ POST Create a new survey
/api/surveys/id/ GET Retrieve a survey by id
/api/surveys/id/ PUT Update a survey's choice selections

In the next article I will be demonstrating how to integrate the frontend Vue.js application so that it can consume and push data updates to the Flask backend.

As always, thanks for reading and don't be shy about commenting or critiquing below.

Author image
Lincoln, Nebraska Twitter