Flask-RESTful – How to quickly build API
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
- Installed Python 3.x
- Code editor of your choice (e.g. PyCharm, Visual Studio Code)
Project setup
- Create new project
mkdir drivers-api cd drivers-api
- Create virtual environment
python3 -m venv flask_venv
- Activate created venv
source flask_venv/bin/activate
- Install required dependencies
- Manually with use of pip
pip install flask pip install flask-restful
- Alternatively, you can create a requirements.txt file with dependencies:
flask flask-restful
And install them with command:pip install -r requirements.txt
- Manually with use of pip
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.
- 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
- 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, '/')
- 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
- To start the application, we just need to run our script:
python minimal-api.py
- 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.
- 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'}
}
- 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.
- 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.
- 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:
- Export FLASK_APP variable
export FLASK_APP=fuel-consumption-api.py
- Run flask
flask run
- 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) „`
- To run application in development mode FLASK_ENV variable needs to be set
export FLASK_ENV=development flask run
- 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
- 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.
- 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
- 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
- To convert arguments to dictionary entry parserrequestbody method is implemented
def parse_record_from_json(args):
return {'odometer': args['odometer'], 'fuelQuantity': args['fuelQuantity']}
- There is no need to add new resource to api, as all resources are already added. New methods are handled automatically.
Testing
- 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
- 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.
- 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
}
- 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
- 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.
- 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')
- 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
- User is able to check last stored fuel consumption
Query:curl -X GET http://localhost:5000/calculateLastConsumption
Expected response:
{
"lastFuelConsumption": 9.0
}
- User is able to check average fuel consumption, based on all stored records
Querycurl -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.