Skip to main content

In depth: Item resources

tip

Resources are definitions of how users interact with our application, so we define them to deal with user requests. Resources will use Models to communicate with other parts of the application and to interact with the database.

Each Resource is a class. Specially named methods defined inside it are used by Flask-RESTful to respond to user request. The get method (which can be instance, static, or class method) is used to respond to GET requests. Similarly we can define other methods such as post, delete, or put.

The Item Resource has:

  • get(name): clients can use this to retrieve information about one item;
  • post(name): clients can use this to create an item;
  • put(name): clients can use this to update an item, or create it if it doesn't exist; and
  • delete(name): clients can use this to delete an item.

There's also an ItemList Resource that has:

  • get(): clients can use this to retrieve data about all items in our collection.

Code for resources/item.py

from flask_restful import Resource, reqparse
from flask_jwt_extended import (
jwt_required,
get_jwt_claims,
get_jwt_identity,
jwt_optional,
fresh_jwt_required,
)
from models.item import ItemModel


class Item(Resource):
parser = reqparse.RequestParser()
parser.add_argument(
"price", type=float, required=True, help="This field cannot be left blank!"
)
parser.add_argument(
"store_id", type=int, required=True, help="Every item needs a store_id."
)

@jwt_required
def get(self, name):
item = ItemModel.find_by_name(name)
if item:
return item.json(), 200
return {"message": "Item not found."}, 404

@fresh_jwt_required
def post(self, name):
if ItemModel.find_by_name(name):
return (
{"message": "An item with name '{}' already exists.".format(name)},
400,
)

data = Item.parser.parse_args()
item = ItemModel(name, **data)

try:
item.save_to_db()
except:
return {"message": "An error occurred while inserting the item."}, 500

return item.json(), 201

@jwt_required
def delete(self, name):
claims = get_jwt_claims()
if not claims["is_admin"]:
return {"message": "Admin privilege required."}, 401

item = ItemModel.find_by_name(name)
if item:
item.delete_from_db()
return {"message": "Item deleted."}, 200
return {"message": "Item not found."}, 404

def put(self, name):
data = Item.parser.parse_args()

item = ItemModel.find_by_name(name)

if item:
item.price = data["price"]
else:
item = ItemModel(name, **data)

item.save_to_db()

return item.json(), 200


class ItemList(Resource):
@jwt_optional
def get(self):
user_id = get_jwt_identity()
items = [item.json() for item in ItemModel.find_all()]
if user_id:
return {"items": items}, 200
return (
{
"items": [item["name"] for item in items],
"message": "More data available if you log in.",
},
200,
)

As well as the method definitions, resources often contain a way to parse incoming data. When users make requests to our API, they can send data in the request body. This normally is either as form data or as JSON.

tip

Check out the JWT Authentication feature page for information on the decorators used in this code (@jwt_required, @fresh_jwt_required, and @jwt_optional) and on get_jwt_identity() and get_jwt_claims().

This code here uses reqparse to define an object which can understand both ways of incoming data. Then when we use parser.parse_args(), it goes into the body of the request and extracts the data.

@fresh_jwt_required
def post(self, name):
if ItemModel.find_by_name(name):
return (
{"message": "An item with name '{}' already exists.".format(name)},
400,
)

data = Item.parser.parse_args()
item = ItemModel(name, **data)

try:
item.save_to_db()
except:
return {"message": "An error occurred while inserting the item."}, 500

return item.json(), 201

The Item resource

The path to understand what the Item resource does starts in app.py, when the resource is registered:

api.add_resource(Store, "/store/<string:name>")
api.add_resource(StoreList, "/stores")
api.add_resource(Item, "/item/<string:name>")
api.add_resource(ItemList, "/items")
api.add_resource(UserRegister, "/register")

This defines that the URL to access the Item resource is /item/<string:name>, which means that it could be /item/chair, or any other string in the second part of the path.

The methods defined in the resource can then make use of that string—in our case we use that string as the name of the item (to either retrieve the item from the database, to create a new one, or to delete one).

The ItemList resource does not have such a parameter because it does not need the name of any specific item, since it returns information on all items.

Get an item

To get an item, the user must provide a valid JWT since the method is marked as @jwt_required. More information in the JWT Authentication feature page:

@jwt_required
def get(self, name):
item = ItemModel.find_by_name(name)
if item:
return item.json(), 200
return {"message": "Item not found."}, 404

Then this method goes into the database to find an item by the name provided in the URL, and returns its JSON dictionary if it is available, or a message otherwise. Instead of just returning item.json() we can return a tuple from these methods, and Flask-RESTful will use the second element of the tuple as the response's status code.

If we found an item successfully in the database for example, we would return the item's JSON alongside a status code of 200 (which means OK).

If we did not find the item, we return a message along a status code of 404 (which means not found).

tip

More information on HTTP status codes can be found in the FAQ, under the "What are some common HTTP status codes?" question.

Create an item

Creating an item requires a fresh JWT since for the sake of example we've decided to treat this endpoint as a "highly sensitive" endpoint:

@fresh_jwt_required
def post(self, name):
if ItemModel.find_by_name(name):
return (
{"message": "An item with name '{}' already exists.".format(name)},
400,
)

data = Item.parser.parse_args()
item = ItemModel(name, **data)

try:
item.save_to_db()
except:
return {"message": "An error occurred while inserting the item."}, 500

return item.json(), 201

In broad terms this means this must be the first JWT the user requests since logging in, and cannot be a JWT generated via the Token Refresh functionality. This ensures that the user just entered their password.

A fresh token is the most secure token since it means the user just authenticated. More information in the Token Refresh feature page.

This method then:

  1. Finds an item in the database;
    1. If it exists, return an error message.
  2. Otherwise, parse the arguments that are in the request body (such as the price and store_id);
  3. Create the item and save it to the database;
    1. Return an error message if that failed.
  4. Return the newly created item's JSON, alongside a 201 status code (which means CREATED).

Delete an item

To delete an item, we also require a JWT. However, the JWT must have a claim stating that is_admin is True.

tip

Claims are arbitrary pieces of data that can be included in the JWT when it is created. More information available in the JWT Authentication feature page.

If the claim is not present or is not True, then we return an error message and a 401 status code which means UNAUTHORIZED.

@jwt_required
def delete(self, name):
claims = get_jwt_claims()
if not claims["is_admin"]:
return {"message": "Admin privilege required."}, 401

item = ItemModel.find_by_name(name)
if item:
item.delete_from_db()
return {"message": "Item deleted."}, 200
return {"message": "Item not found."}, 404

Otherwise we go ahead and find the item and delete, returning appropriate messages and status codes.

Update an item

Updating an item is the only method in the resource that can do two things. It can either create a new item, or update it if it already exists.

tip

PUT requests are meant to be idempotent, which means that you can repeat a request many times and the server will always end up with the same data, no matter how many times you run it.

For example if you make the same POST request many times, the second time will give you an error saying the item already exists. If you make the same PUT request many times, the first one will create a new object and subsequent ones will update it to have the same data it already has—so the server will end up with the same data no matter how many times you run the request.

The code for this request is as follows:

def put(self, name):
data = Item.parser.parse_args()

item = ItemModel.find_by_name(name)

if item:
item.price = data["price"]
else:
item = ItemModel(name, **data)

item.save_to_db()

return item.json(), 200
  1. Retrieve data from the request body using the parser;
  2. Find the item by its name;
  3. If it exists, update its price (that's the only thing we allow updates to);
  4. Otherwise, create a new item;
  5. Save it to the database;
  6. Return the item's JSON.

The ItemList resource

The ItemList resource makes use of a @jwt_optional decorator which can be very handy if you want loggedd-out users to be able to see some data, but allow logged-in users see more data.

In the get() method of this resource we retrieve a list of all items across all our stores.

If the user is logged out, they get to see item names only. If they are logged in, they get to see the full item JSON (id, name, price, and store_id):

class ItemList(Resource):
@jwt_optional
def get(self):
user_id = get_jwt_identity()
items = [item.json() for item in ItemModel.find_all()]
if user_id:
return {"items": items}, 200
return (
{
"items": [item["name"] for item in items],
"message": "More data available if you log in.",
},
200,
)

The way we do this is by using get_jwt_identity(). This gives us a user ID if we received a JWT in the request. If we did not receive a JWT, we get None back—which means the user is not logged in.

Then we if we have a user_id, we return the full item payload. Otherwise we just return item names and a message saying there's more information available for logged-in users.

This is a very popular pattern, and very easy to implement using Flask-JWT-Extended.