Rest API development with Python, Flask, MongoDB, Docker | Part 3
In the previous tutorial we dockerized our project and improved our models.
Today we are going to implement flask_marshmallow extension (an object serialization/deserialization library).
Also we are going to perform CRUD operations.
We were getting our response as the following.
[
{
"_id": {
"$oid": "63b9d62bd1627fb9544b8833"
},
"author": {
"$oid": "63b9d5ded1627fb9544b8831"
},
"name": "Flask 101",
"published_year": 2023
},
{
"_id": {
"$oid": "63b9d63dd1627fb9544b8834"
},
"author": {
"$oid": "63b9d601d1627fb9544b8832"
},
"name": "Angular 101",
"published_year": 2023
},
{
"_id": {
"$oid": "63b9d64dd1627fb9544b8835"
},
"author": {
"$oid": "63b9d5ded1627fb9544b8831"
},
"name": "Django 101",
"published_year": 2025
},
{
"_id": {
"$oid": "63b9d65cd1627fb9544b8836"
},
"author": {
"$oid": "63b9d5ded1627fb9544b8831"
},
"name": "Scrapy 101",
"published_year": 2024
}
]
Let’s remember the project structure again
Flask_MongoDB_Docker $ tree . -I env311
.
├── Dockerfile
├── README.md
├── __pycache__
├── api
│ ├── __init__.py
│ ├── __pycache__
│ ├── app.py
│ ├── models.py
│ ├── schemas.py
│ └── views.py
├── config.py
├── docker-compose.yml
├── main.py
└── requirements.txt
3 directories, 11 files
Let’s implement flask marshmallow. Open up api/app.py and import flask marshmallow
from flask import Flask
from flask_mongoengine import MongoEngine
from flask_marshmallow import Marshmallow # new
# internals
from config import Config
db = MongoEngine()
marsh = Marshmallow() # new
def create_app(config_class=Config):
# existing code ...
# serialization/deserialization Marshmallow
marsh.init_app(app) # new
# existing code ...
return app
And edit api/__init__.py as below
from api.app import create_app, db , marsh
Now open api/schemas.py and create AuthorSchema and BookSchema classes and add their fields(remember that we defined these fields in model classes too) for serialization/deserialization. So we want to serialize all model fields and we defined in schema classes. If you want to serialize only desired fields, you can define only whatever you want.
from marshmallow import validate
# internals
from api import marsh as ma
class AuthorSchema(ma.Schema):
id = ma.String(unique=True, dump_only=True)
firstname = ma.String(required=True)
lastname = ma.String(required=True)
class Meta:
ordered = True
class BookSchema(ma.Schema):
id = ma.String(unique=True, dump_only=True)
name = ma.String(required=True)
author = ma.Nested(AuthorSchema, dump_only=True)
published_year = ma.Integer(strict=True, required=True,
validate=[validate.Range(
min=610, error="Year must be min 610")]
)
class Meta:
ordered = True
The AuthorSchema
class defines three fields, id
, firstname
, and lastname
, all of which are strings. The id
field is marked as unique and is only included in the serialized output (dump_only=True
).
The BookSchema
class defines four fields, id
, name
, author
and published_year
. id
field is marked as unique and is only included in the serialized output. The name
field is a required string. The author
field is nested, meaning it is an instance of the AuthorSchema
class and it is also only included in the serialized output. The published_year
field is an integer, it is required and will only accept integers and will raise error if a non-integer value is passed. It is also validated with a range validation, where year must be minimum 610.
Both classes have a Meta
class that sets ordered
to True
. This means order serialization output according to the order in which fields were declared.
Now it is time to use these schemas in api/views.py
from flask import Blueprint, jsonify
from apifairy import response # new
# internals
from .models import Book
from .schemas import AuthorSchema, BookSchema # new
books_bp = Blueprint("books", __name__)
books_schema = BookSchema(many=True) # new
@books_bp.route("/books/", methods=["GET"])
@response(books_schema) # new
def books():
book_list = Book.objects.all()
return book_list
We import a response
decorator from apifairy
which is a custom decorator. Also we import AuthorSchema and BookSchema that we defined previously . And then we create an instance of the BookSchema
class with the many
parameter set to True
, which tells marshmallow that it will be serializing a list of objects. The response
decorator is a custom decorator that takes the books_schema
as an argument and it will automatically handle the serialization of the returned data before sending it as a response.
Let’s make a GET request to http://127.0.0.1:5000/api/v1/books/ and see the response. You should see the response like this
[
{
"author": {
"firstname": "Adnan",
"id": "63b9d5ded1627fb9544b8831",
"lastname": "Kaya"
},
"id": "63b9d62bd1627fb9544b8833",
"name": "Flask 101",
"published_year": 2023
},
{
"author": {
"firstname": "Recep",
"id": "63b9d601d1627fb9544b8832",
"lastname": "Alkurt"
},
"id": "63b9d63dd1627fb9544b8834",
"name": "Angular 101",
"published_year": 2023
},
{
"author": {
"firstname": "Adnan",
"id": "63b9d5ded1627fb9544b8831",
"lastname": "Kaya"
},
"id": "63b9d64dd1627fb9544b8835",
"name": "Django 101",
"published_year": 2025
},
{
"author": {
"firstname": "Adnan",
"id": "63b9d5ded1627fb9544b8831",
"lastname": "Kaya"
},
"id": "63b9d65cd1627fb9544b8836",
"name": "Scrapy 101",
"published_year": 2024
}
]
Create Author
Now let’s add /authors/ endpoint and its view function as create_author to make POST request. Open api/views.py
from flask import Blueprint, jsonify
from apifairy import response, body # new
# internals
from .models import Book, Author # new
from .schemas import AuthorSchema, BookSchema
books_bp = Blueprint("books", __name__)
books_schema = BookSchema(many=True)
author_schema = AuthorSchema() # new
@books_bp.route("/authors/", methods=["POST"])
@body(author_schema)
@response(author_schema, status_code=201)
def create_author(payload):
author = Author(**payload)
author.save()
return author
The create_author
function is decorated with three decorators. The first is @books_bp.route("/authors/", methods=["POST"])
which maps the route '/authors/' to this function and allows only POST requests to this endpoint. The second decorator is @body(author_schema)
which validates the request body against the author_schema
and it will parse the request body and pass it as a payload
argument to the function. The third decorator is @response(author_schema, status_code=201)
which tells the decorator to serialize the returned data using the author_schema
and set the status code of the response to 201 which indicates that the resource was successfully created.
Now let’s make a POST request to http://127.0.0.1:5000/api/v1/authors/ with the request body:
{
"firstname":"Adnan2",
"lastname":"Kaya2"
}
We will get the response as below
{
"firstname": "Adnan2",
"id": "63c31e4110bf2407b4c2eb89",
"lastname": "Kaya2"
}
Get Authors
Let’s create an endpoint and its view function to get author list. api/views.py
# existings ...
author_schema = AuthorSchema()
authors_schema = AuthorSchema(many=True) # new
@books_bp.route("/authors/", methods=["GET"])
@response(authors_schema)
def authors():
author_list = Author.objects.all()
return author_list
Make a GET request to http://127.0.0.1:5000/api/v1/authors/ and response will be
[
{
"firstname": "Adnan",
"id": "63b9d5ded1627fb9544b8831",
"lastname": "Kaya"
},
{
"firstname": "Recep",
"id": "63b9d601d1627fb9544b8832",
"lastname": "Alkurt"
},
{
"firstname": "Adnan2",
"id": "63c31e4110bf2407b4c2eb89",
"lastname": "Kaya2"
},
{
"firstname": "Adnan3",
"id": "63c31e9c33f15afb371e3144",
"lastname": "Kaya3"
},
{
"firstname": "Adnan4",
"id": "63c31f8dafd3ee10e7cc48da",
"lastname": "Kaya4"
}
]
Create Book
Now let’s create /books/ endpoint to make POST request for adding new book. Before creating new function we need to edit BookSchema. api/schemas.py
class BookSchema(ma.Schema):
# existings ...
author_id = ma.String(load_only=True, required=True)# new
# existings ...
It defines a new field author_id
which is a string and it is marked as load_only=True
and required=True
, this means that this field will only be present in the deserialized input and it is required. It means that when a JSON payload is sent to the server, this field is expected to be present and it will be used to identify the author associated with the book.
This is useful in this case because it allows you to enforce validation on the input data, ensuring that the client sends a valid author_id before creating a new book.
It’s also worth noting that this field is not present in the serialized output, so it won’t be included in the response to the client.
Open up api/views.py
from flask import Blueprint, jsonify, abort # new
# existings ...
books_schema = BookSchema(many=True)
book_schema = BookSchema() # new
# existings ...
@books_bp.route("/books/", methods=["POST"])
@body(book_schema)
@response(book_schema, status_code=201)
def create_book(payload):
author_id = payload.pop("author_id")
try:
author = Author.objects.get(id=author_id)
except Author.DoesNotExist as exc:
return abort(code=404,
description="Author does not exist, check your author_id again")
except:
return abort(code=400)
else:
book = Book(**payload, author=author)
book.save()
return book
The create_book
function starts by extracting the author_id
from the payload and then it tries to retrieve the corresponding Author
object from the database by id. If the Author
does not exist it will return a response with a status code of 400 and a "description" of "Author does not exist, check your author_id again". If the Author
does exist, the function creates a new Book
object with the remaining payload data and the author
attribute set to the retrieved Author
object, it then saves it to the database and returns the newly created book object.
Now make a GET request to http://127.0.0.1:5000/api/v1/authors/ and grab one of the author’s id. I took Adnan2’s id.
Now make a POST request to http://127.0.0.1:5000/api/v1/books/ with the request body
{
"name":"Rest API development with Flask, MongoDB, Docker",
"published_year":2023,
"author_id": "63c31e4110bf2407b4c2eb89"
}
Delete Author by ID
open api/views.py and add the following
@books_bp.route("/authors/<string:id>", methods=["DELETE"])
def delete_author(id):
try:
author = Author.objects.get(id=id)
author.delete()
return jsonify(message=f"Deleted {author}!")
except Author.DoesNotExist as ex:
return abort(404, description=ex)
except:
return abort(400)
Grab one of the author ID and make HTTP DELETE request like http://127.0.0.1:5000/api/v1/authors/63c31e9c33f15afb371e3144
Update Author by ID (PUT , PATCH request)
Open api/views.py
# existings ..
author_schema = AuthorSchema()
authors_schema = AuthorSchema(many=True)
update_author_schema = AuthorSchema(partial=True)# new
# existings ...
@books_bp.route("/authors/<string:id>", methods=["PUT", "PATCH"])
@body(update_author_schema)
@response(author_schema, status_code=200)
def update_author(payload, id):
try:
author = Author.objects.get(id=id)
author.update(**payload)
author.save()
author.reload()
return author
except Author.DoesNotExist as ex:
return abort(404, description=ex)
except:
return abort(400)
The update_author
function starts by trying to retrieve the corresponding Author
object from the database by id. If the Author
does not exist it will return a response with a status code of 404 and a "description" of the exception message. If the Author
exists, it updates the fields of the author object using the payload data and saves it to the database and reloads the author from the database and return the updated author object.
If any other exception occurs it will return an abort with a status code of 400.
Let’s make PUT request to http://127.0.0.1:5000/api/v1/authors/63c31e4110bf2407b4c2eb89 with the request body
{
"firstname": "Adnan22",
"lastname": "Kaya22"
}
The response
{
"firstname": "Adnan22",
"id": "63c31e4110bf2407b4c2eb89",
"lastname": "Kaya22"
}
Make PATCH request with the request body
{
"lastname": "Kaya22 | only updated last name"
}
Response
{
"firstname": "Adnan22",
"id": "63c31e4110bf2407b4c2eb89",
"lastname": "Kaya22 | only updated last name"
}
Conclusion
Today we performed CRUD operations on our rest api project and also did serialization/deserialization.
You can access the github repo in here.
You can read previous tutorial here.
If you want to read some information about me and contact, visit my github page: https://adnankaya.github.io/
Hopefully you have learned something new from this series