Rest API development with Python, Flask, MongoDB, Docker | Part 3

Adnan Kaya
7 min readJan 14, 2023

--

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

--

--