Flask-RESTful – How to quickly build API

Jacek Zygiel

REST architecture is currently very widely used. There are many frameworks which allows developer to easily build REST api. Flask is a micro-framework which provides tools that allows to build a web service. Flask-RESTful is an extension to Flask microframework, which simplify creation of REST API.

REpresentational State Transfer (REST)

REST is an architecture concept used to creating APIs, which uses HTTP methods. The main architecture constraints of REST are:

  • Client-server architecture – client and server layers are separated. It makes REST Api portable – api can be consumed by different clients and different platforms.
  • Statelessness – request from client to server must contains all information to understand the request.
  • Cacheability – server is never generating the same response twice, as it’s cached.
  • Layered system – every layer has a single purpose.

REST methods

GET– retrieve specific resource or a set of resources

POST– creates a new resource

PUT– updates an existing resource or creates new resources

DELETE– removes resource

Prerequisite

  1. Installed Python 3.x
  2. Code editor of your choice (e.g. PyCharm, Visual Studio Code)

Project setup

  1. Create new project mkdir drivers-api cd drivers-api
  2. Create virtual environment python3 -m venv flask_venv
  3. Activate created venv source flask_venv/bin/activate
  4. Install required dependencies
    1. Manually with use of pip pip install flask pip install flask-restful
    2. Alternatively, you can create a requirements.txt file with dependencies: flask flask-restful And install them with command: pip install -r requirements.txt

Minimum API

Code:

from flask import Flask
from flask_restful import Resource, Api

app = Flask(__name__)
api = Api(app)


class HelloJlabs(Resource):
    def get(self):
        return {'hello': 'j-labs'}


api.add_resource(HelloJlabs, '/')

if __name__ == '__main__':
    app.run(debug=True)

Code description

Above example shows how little code is needed to create working API.

  1. FlaskRESTful is based on the Flask so, we need to import flask and flaskrestful dependencies. python from flask import Flask from flask_restful import Resource, Api
  2. As the next step, we need to initialize Flask and Flask-RESTful Api objects. python app = Flask(__name__) api = Api(app)
    • Object HelloJlabs with the definition of the get method needs to be added to api object as a resource, with second parameter – url.
      class HelloJlabs(Resource):
      def get(self):
          return {'hello': 'j-labs'}
      
      
      api.add_resource(HelloJlabs, '/')
      1. The last step is to allow us to run Flask application by simply running python script. python if __name__ == '__main__': app.run(debug=True)

      Start the minimum api

      1. To start the application, we just need to run our script: python minimal-api.py
      2. Open the http://localhost:5000 in your browser to get the result: json { "hello": "j-labs" }

      Drivers project

      We’ll create an API to monitor a fuel consumption of our car. Business requirements for our application:

      • User is able to save a record with following required fields presented in json format
        • odometer – odometer value read from dashboard
        • fuelQuantity – the quantity of fuel refueled on gas station
      • User is presented with an error message following sent request without required fields
      • User is able to update an existing record
      • User is able to retrieve a single record
      • User is able to retrieve the list of all records
      • User is able to delete a single record
      • User is able to check last stored fuel consumption
      • User is able to check average fuel consumption, based on all stored records

      For the introduction to Flask-RESTful microframework to minimalize configuration as a storage for data, I will use a Python dictionary.

      Get methods implementation

      from flask import Flask
      from flask_restful import Resource, Api, abort, reparse
      
      app = Flask(__name__)
      api = Api(app)
      
      FUEL_CONSUMPTION = {
          '1': {'odometer': 0,
                'fuelQuantity': 0.0},
          '2': {'odometer': 100,
                'fuelQuantity': 12.5},
          '3': {'odometer': 300,
                'fuelQuantity': 30.0},
          '4': {'odometer': 400,
                'fuelQuantity': 8.5},
          '5': {'odometer': 500,
                'fuelQuantity': 9}
      }
      
      
      def abort_if_record_doesnt_exist(record_id):
          if record_id not in FUEL_CONSUMPTION:
              abort(404, message="Record {} doesn't exist".format(record_id))
      
      
      api.add_resource(FuelConsumptionList, '/recordList',
                                            '/')
      api.add_resource(FuelConsumption, '/record/<record_id>')
      
      if __name__ == '__main__':
          app.run(debug=True)

      Code description

      Above code is complete example of fuel consumption api, which implements both of retrieve methods described in business requirements.

      1. Python dictionary with examples of records is created:
      FUEL_CONSUMPTION = {
      '1': {'odometer': '0',
            'fuelQuantity': '0'},
      '2': {'odometer': '100',
           'fuelQuantity': '12'},
      '3': {'odometer': '300',
            'fuelQuantity': '30'},
      '4': {'odometer': '400',
            'fuelQuantity': '8'},
      '5': {'odometer': '500',
            'fuelQuantity': '9'}
      }
      1. Method which returns a status 404 with proper response is created:
      def abort_if_record_doesnt_exist(record_id):
      if record_id not in FUEL_CONSUMPTION:
          abort(404, message="Record {} doesn't exist".format(record_id))

      If this method will be omitted, in case when record with given id will be not available, internal server error (http status code 500) will be returned. We should avoid internal server errors, as they didn’t gives any feedback to the user to properly handle such case.

      1. Method which returns a status 404 with proper response is created:
        class FuelConsumptionList(Resource):
            def get(self):
                return {"fuelConsumption": FUEL_CONSUMPTION}
        
        
        class FuelConsumption(Resource):
            def get(self, record_id):
                abort_if_record_doesnt_exist(record_id)
                return FUEL_CONSUMPTION[record_id)

        Both of the classes define get methods to retrieve data.

        1. To access to the above classes, there is a need to add them as a resource to api object with url mapping.
        api.add_resource(FuelConsumptionList, '/recordList',
                                              '/')
        api.add_resource(FuelConsumption, '/record/<record_id>') 

        FuelConsumption resource includes path parameter record_id which is passed to get method as a parameter. In FuelConsumptionList, there are two urls set, so user is able to get this data with /recordList and / endpoint urls.

        Start application

        To start Flask application in proper way two steps are required:

        1. Export FLASK_APP variable

        export FLASK_APP=fuel-consumption-api.py

        1. Run flask flask run
        2. Application will be stared „`
          • Serving Flask app „fuel-consumption-api.py”
          • Environment: production WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
          • Debug mode: off
          • Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) „`
        3. To run application in development mode FLASK_ENV variable needs to be set export FLASK_ENV=development flask run
        4. Now application will be started in development mode. It’s highly desired as following features will be activated:
        • Debbuger
        • Automatic reloader
        • Debug mode

        Testing

        Let’s check if business requirements for retrieving data are met

        1. User is able to retrieve a single record
          Query:
          curl -X GET http://localhost:5000/recordList
          Expected response:
        {
        "fuelConsumption": {
             "1": {
                 "odometer": 0,
                 "fuelQuantity": 0.0
             },
             "2": {
                 "odometer": 100,
                 "fuelQuantity": 12.5
             },
             "3": {
                 "odometer": 300,
                 "fuelQuantity": 30.0
             },
             "4": {
                 "odometer": 400,
                 "fuelQuantity": 8.5
             },
             "5": {
                 "odometer": 500,
                 "fuelQuantity": 9
             }
         }
        } 

        All stored data is successfully retrieved as a json.

        1. User is able to retrieve a single record
          Query:
          curl -X GET http://localhost:5000/record/2
          Expected response:
        {
         "odometer": 100,
         "fuelQuantity": 12.5
        }

        Single record data is successfully retrieved.

        Record, update and delete methods implementation

        from flask import Flask
        from flask_restful import Resource, Api, abort, reqparse
        
        app = Flask(__name__)
        api = Api(app)
        
        FUEL_CONSUMPTION = {
            '1': {'odometer': 0,
                  'fuelQuantity': 0.0},
            '2': {'odometer': 100,
                  'fuelQuantity': 12.5},
            '3': {'odometer': 300,
                  'fuelQuantity': 30.0},
            '4': {'odometer': 400,
                  'fuelQuantity': 8.5},
            '5': {'odometer': 500,
                  'fuelQuantity': 9}
        }
        
        
        parser = reqparse.RequestParser()
        parser.add_argument('odometer', type=float, required=True)
        parser.add_argument('fuelQuantity', type=float, required=True)
        
        
        def abort_if_record_doesnt_exist(record_id):
            if record_id not in FUEL_CONSUMPTION:
                abort(404, message="Record {} doesn't exist".format(record_id))
        
        def parse_record_body(args):
            return {'odometer': args['odometer'], 'fuelQuantity': args['fuelQuantity']}
        
        
        class FuelConsumptionList(Resource):
            def get(self):
                return {"fuelConsumption": FUEL_CONSUMPTION}
        
            def post(self):
                record_id = str(int(max(FUEL_CONSUMPTION.keys())) + 1)
                FUEL_CONSUMPTION[record_id] = parse_request_body(parser.parse_args())
                return FUEL_CONSUMPTION[record_id]
        
        
        class FuelConsumption(Resource):
            def get(self, record_id):
                abort_if_record_doesnt_exist(record_id)
                return FUEL_CONSUMPTION[record_id]
        
            def delete(self, record_id):
                abort_if_record_doesnt_exist(record_id)
                del FUEL_CONSUMPTION[record_id]
                return '', 204
        
            def put(self, record_id):
                abort_if_record_doesnt_exist(record_id)
                record = parse_request_body(parser.parse_args())
                FUEL_CONSUMPTION[record_id] = record
                return record, 201
        
        
        api.add_resource(FuelConsumptionList, '/recordList',
                                              '/')
        api.add_resource(FuelConsumption, '/record/<record_id>')
        
        if __name__ == '__main__':
            app.run(debug=True)

        Code description

        1. Based on documentation Request parser in Flask-RESTful is a simple solution to access variable on flask.request object.
          parser = reqparse.RequestParser()
          parser.add_argument('odometer', type=float, required=True)
          parser.add_argument('fuelQuantity', type=float, required=True)

          Parser read arguments from request body and store it in given type (string is a default value) Requirement to make fields required is met with add_argument parameter required=True

          1. To convert arguments to dictionary entry parserrequestbody method is implemented
          def parse_record_from_json(args):
             return {'odometer': args['odometer'], 'fuelQuantity': args['fuelQuantity']}
          1. There is no need to add new resource to api, as all resources are already added. New methods are handled automatically.

          Testing

          1. User is able to save a record 

          Query:

          curl -X POST \
          http://localhost:5000/recordList \
          -H 'Content-Type: application/json' \
          -d '{
          "odometer": 805,
          "fuelQuantity":9
          }'

          Expected response:

          {
          "odometer": 805,
          "fuelQuantity": 9
          }

          Response with 201 http status code (CREATED) is returned

          1. User is presented with an error message following sent request without required fields

          Query:

          curl -X POST \
          http://localhost:5000/recordList \
          -H 'Content-Type: application/json' \
          -d '{}'

          Expected response: 400 BAD REQUEST http response code with body

          {
              "message": {
                  "odometer": "odometer is required parameter!",
                  "fuelQuantity": "fuelQuantity is required parameter!"
              }
          }

          Current response:

          {
              "message": {
                  "odometer": "odometer is required parameter!"
              }
          }

          By default error, messages are not bundled. After the first occurrence of error, the response will be returned. To get bundled errors we need to set a parameter while initializing reparse object.

          parser = reqparse.RequestParser(bundle_errors=True) 

          Now current response is equal to expected.

          1. User is able to update existing record

          Query:

          curl -X PUT \
          http://localhost:5000/record/5 \
          -H 'Content-Type: application/json' \
          -d '{
          "odometer": 620,
          "fuelQuantity": 20
          }'

          Expected response:

          {
          "odometer": 620,
          "fuelQuantity": 20
          }
          1. User is able to delete a single record 

          Query:

          curl -X DELETE http://localhost:5000/record/2

          Response 
          Response with 204 http status code (NO_CONTENT) is returned

          1. To check if above operations are successful, call with GET method /recordList endpoint

          Query:

          curl -X GET http://localhost:5000/recordList

          Expected response: Valid response body is presented below:

          {
          "fuelConsumption": {
              "1": {
                  "odometer": 0,
                  "fuelQuantity": 0.0
              },
              "3": {
                  "odometer": 300,
                  "fuelQuantity": 30.0
              },
              "4": {
                  "odometer": 400,
                  "fuelQuantity": 8.5
              },
              "5": {
                  "odometer": 620,
                  "fuelQuantity": 20
              },
              "6": {
                  "odometer": 805,
                  "fuelQuantity": 9
              }
          }
          } 

          Statistic methods implementation

          from flask import Flask
          from flask_restful import Resource, Api, abort, reqparse
          
          app = Flask(__name__)
          api = Api(app)
          
          FUEL_CONSUMPTION = {
              '1': {'odometer': 0,
                    'fuelQuantity': 0.0},
              '2': {'odometer': 100,
                    'fuelQuantity': 12.5},
              '3': {'odometer': 300,
                    'fuelQuantity': 30.0},
              '4': {'odometer': 400,
                    'fuelQuantity': 8.5},
              '5': {'odometer': 500,
                    'fuelQuantity': 9}
          }
          
          
          parser = reqparse.RequestParser(bundle_errors=True)
          parser.add_argument('odometer', type=float, required=True, help="odometer is required parameter!")
          parser.add_argument('fuelQuantity', type=float, required=True, help="fuelQuantity is required parameter!")
          
          
          def abort_if_record_doesnt_exist(record_id):
              if record_id not in FUEL_CONSUMPTION:
                  abort(404, message="Record {} doesn't exist".format(record_id))
          
          
          def get_record_by_order(order):
              keys_list = list(FUEL_CONSUMPTION.keys())
              return FUEL_CONSUMPTION[keys_list[order]]
          
          
          def calculate_consumption(fuel_quantity, distance):
              return fuel_quantity / distance * 100
          
          
          def calculate_distance(start_odometer, end_odometer):
              return end_odometer - start_odometer
          
          
          def parse_request_body(args):
              return {'odometer': args['odometer'], 'fuelQuantity': args['fuelQuantity']}
          
          
          class FuelConsumptionList(Resource):
              def get(self):
                  return {"fuelConsumption": FUEL_CONSUMPTION}
          
              def post(self):
                  record_id = str(int(max(FUEL_CONSUMPTION.keys())) + 1)
                  FUEL_CONSUMPTION[record_id] = parse_request_body(parser.parse_args())
                  return FUEL_CONSUMPTION[record_id], 201
          
          
          class FuelConsumption(Resource):
              def get(self, record_id):
                  abort_if_record_doesnt_exist(record_id)
                  return FUEL_CONSUMPTION[record_id]
          
              def delete(self, record_id):
                  abort_if_record_doesnt_exist(record_id)
                  del FUEL_CONSUMPTION[record_id]
                  return '', 204
          
              def put(self, record_id):
                  abort_if_record_doesnt_exist(record_id)
                  record = parse_request_body(parser.parse_args())
                  FUEL_CONSUMPTION[record_id] = record
                  return record, 201
          
          
          class LastFuelConsumption(Resource):
              def get(self):
                  last_record = get_record_by_order(-1)
                  second_last_record = get_record_by_order(-2)
                  distance = calculate_distance(second_last_record['odometer'], last_record['odometer'])
                  consumption = calculate_consumption(last_record['fuelQuantity'], distance)
                  return {'lastFuelConsumption': consumption}
          
          
          class AverageFuelConsumption(Resource):
              def get(self):
                  records_count = len(FUEL_CONSUMPTION)
                  sum_of_consumptions = 0
                  for i in reversed(range(0, records_count)):
                      start_record = get_record_by_order(i - 1)
                      end_record = get_record_by_order(i)
                      distance = calculate_distance(start_record['odometer'], end_record['odometer'])
                      sum_of_consumptions += calculate_consumption(end_record['fuelQuantity'], distance)
                  return {"avgFuelConsumption": sum_of_consumptions / (records_count - 1)}
          
          
          api.add_resource(FuelConsumptionList, '/recordList',
                                                '/')
          api.add_resource(FuelConsumption, '/record/<record_id>')
          api.add_resource(LastFuelConsumption, '/calculateLastConsumption')
          api.add_resource(AverageFuelConsumption, '/calculateAverageConsumption')
          
          if __name__ == '__main__':
              app.run(debug=True)
          

          Code description

          Above code met all mentioned business requirements.

          1. Two new endpoints with get methods are added. Their purpose is to meet the following business requirements:
          • User is able to check last stored fuel consumption
          • User is able to check average fuel consumption, based on all stored records
          class LastFuelConsumption(Resource):
              def get(self):
                  last_record = get_record_by_order(-1)
                  second_last_record = get_record_by_order(-2)
                  distance = calculate_distance(second_last_record['odometer'], last_record['odometer'])
                  consumption = calculate_consumption(last_record['fuelQuantity'], distance)
                  return {'lastFuelConsumption': consumption}
          
          
          class AverageFuelConsumption(Resource):
              def get(self):
                  records_count = len(FUEL_CONSUMPTION)
                  sum_of_consumptions = 0
                  for i in reversed(range(0, records_count)):
                      start_record = get_record_by_order(i - 1)
                      end_record = get_record_by_order(i)
                      distance = calculate_distance(start_record['odometer'], end_record['odometer'])
                      sum_of_consumptions += calculate_consumption(end_record['fuelQuantity'], distance)
                  return {"avgFuelConsumption": sum_of_consumptions / (records_count - 1)}
          
          
          api.add_resource(LastFuelConsumption, '/calculateLastConsumption')
          api.add_resource(AverageFuelConsumption, '/calculateAverageConsumption')
          1. To avoid code duplication, common operations are extracted to methods
          def get_record_by_order(order):
              keys_list = list(FUEL_CONSUMPTION.keys())
              return FUEL_CONSUMPTION[keys_list[order]]
          
          
          def calculate_consumption(fuel_quantity, distance):
              return fuel_quantity / distance * 100
          
          
          def calculate_distance(start_odometer, end_odometer):
              return end_odometer - start_odometer

          Testing

          1. User is able to check last stored fuel consumption
            Query:
            curl -X GET http://localhost:5000/calculateLastConsumption
            Expected response:
          {
              "lastFuelConsumption": 9.0
          }
          1. User is able to check average fuel consumption, based on all stored records 
            Query
            curl -X GET http://localhost:5000/calculateAverageConsumption
            Expected response:
          { 
          "avgFuelConsumption": 11.25 
          }

          Summary

          Flask-RESTful is a great way to create REST api with relatively small effort. The article presents the implementation of basic requirements for Fuel Consumption Meter API. All requirements are met with the use of Flask and Flask-RESTful builtin features.

          Sources:

          Poznaj mageek of j‑labs i daj się zadziwić, jak może wyglądać praca z j‑People!

          Skontaktuj się z nami