
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.
Jacek Zygiel |
06 May 2020
## 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:
```python
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. Flask_RESTful is based on the Flask so, we need to import flask and flask_restful 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)
```
3. Object HelloJlabs with the definition of the get method needs to be added to api object as a resource, with second parameter - url.
```python
class HelloJlabs(Resource):
def get(self):
return {'hello': 'j-labs'}
api.add_resource(HelloJlabs, '/')
```
4. 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
```python
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))
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)
api.add_resource(FuelConsumptionList, '/recordList',
'/')
api.add_resource(FuelConsumption, '/record/')
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:
```python
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'}
}
```
2. Method which returns a status 404 with proper response is created:
```python
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.
3. Classes which inherits from Resource class are created for single fuel consumption record
and list of fuel consumption records
```python
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.
4. To access to the above classes, there is a need to add them as a resource to api object with url mapping.
```python
api.add_resource(FuelConsumptionList, '/recordList',
'/')
api.add_resource(FuelConsumption, '/record/')
```
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
```
2. Run flask
```
flask run
```
3. 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)
```
4. To run application in development mode FLASK_ENV variable needs to be set
```
export FLASK_ENV=development
flask run
```
5. 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**:
```json
{
"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.
2. User is able to retrieve a single record
**Query**:
```
curl -X GET http://localhost:5000/record/2
```
**Expected response**:
```json
{
"odometer": 100,
"fuelQuantity": 12.5
}
```
Single record data is successfully retrieved.
#### Record, update and delete methods implementation
```python
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/')
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.
```python
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`
2. To convert arguments to dictionary entry **parser_request_body** method is implemented
```python
def parse_record_from_json(args):
return {'odometer': args['odometer'], 'fuelQuantity': args['fuelQuantity']}
```
3. 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:**
```json
{
"odometer": 805,
"fuelQuantity": 9
}
```
Response with 201 http status code (CREATED) is returned
2. 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
```json
{
"message": {
"odometer": "odometer is required parameter!",
"fuelQuantity": "fuelQuantity is required parameter!"
}
}
```
**Current response:**
```json
{
"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.
```python
parser = reqparse.RequestParser(bundle_errors=True)
```
Now current response is equal to expected.
3. 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:**
```json
{
"odometer": 620,
"fuelQuantity": 20
}
```
4. 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`
5. 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:
```python
{
"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
```python
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/')
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
```python
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')
```
2. To avoid code duplication, common operations are extracted to methods
```python
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:**
```json
{
"lastFuelConsumption": 9.0
}
```
2. User is able to check average fuel consumption, based on all stored records
**Query:**
```
curl -X GET http://localhost:5000/calculateAverageConsumption
```
**Expected response:**
```json
{
"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:
* [https://flask-restful.readthedocs.io](https://flask-restful.readthedocs.io)
* [https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods)
* [https://www.codecademy.com/articles/what-is-rest](https://www.codecademy.com/articles/what-is-rest)
* [https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm](https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm)
Jacek Zygiel
Did you like this article?
1,1 / 82