Adding A Database to Your Application Using Okteto Stacks

Image of Adding A Database to Your Application Using Okteto Stacks

Temporary in-app databases are not ideal for web applications. Any unfortunate event such as an abrupt shutdown or restarting of the application will lead to the total loss of stored data.

This is the third post in our series on how to develop a fastAPI application with Okteto. In the previous posts, you learned how to deploy applications directly from your console using Okteto Stacks and how to deploy it directly from Okteto’s UI.

In this tutorial, you will be adding a database to store better your application data, and then you will deploy the updated version to your Okteto namespace directly from your command line.

Initial Setup

Start by creating a fork of the application from GitHub and then clone it locally.

1
git clone https://github.com/okteto/fastapi-crud

In your local clone, set it up using the following commands:

1
2
3
$ cd fastapi-crud
$ python3 -m venv venv && source venv/bin/activate
(venv)$ pip install -r requirements.txt

Verify that the setup is complete by running:

1
python3 main.py

After verifying the installation, update the DOCKERHUBUSERNAME variable in your stack file to okteto.dev. Deploy the application with the command:

1
(venv)$ okteto stack deploy --build

Creating A New Branch

For this article, we will create a new branch. This is to enable you to differentiate the first post in the series from this.

From your console, run the command:

1
git checkout -b mongo-crud

The command above creates a new branch mongo-crud with the origin pointing to main.

Adding A Database

Before proceeding, verify that you have MongoDB installed or proceed to the MongoDB’s Installation page to install MongoDB.

To avoid data loss, you’re going to rewrite the application logic to use a MongoDB database. Later in the post you’ll deploy the database in your Okteto namespace, but first, let’s run it locally to verify that the code works perfectly.

Start by installing pymongo, a MongoDB driver for Python application, python-decouple for reading environment secrets and update the requirements.txt file:

1
(venv)$ pip install pymongo

Update your requirements.txt file:

1
2
3
...
pymongo
python-decouple

In the api folder, create a new file, database.py, where you’ll write the database CRUD operations’ functions.

Start by importing MongoClient, ObjectID and config:

1
2
3
from pymongo import MongoClient
from bson import ObjectId
from decouple import config

MongoClient is responsible for the connection from our application to the database, ObjectId, on the other hand, is used to pass id values in MongoDB properly, and config is responsible for reading application secrets from .env files.

Next, define the connection, database and database collection details:

1
2
3
4
5
6
7
connection_details = config("DB_HOST")

client = MongoClient(connection_details)

database = client.recipes

recipe_collection = database.get_collection('recipes_collection')

On the first line above, you are using the decouple library to read the environment variable DB_HOST. Create a .env file in the root folder containing the connection detail:

1
DB_HOST=mongodb://localhost:27017

Documents in MongoDB are stored in JSON format and the _id in ObjectId format. Write a function to parse the result from a query:

1
2
3
4
5
6
def parse_recipe_data(recipe) -> dict:
return {
"id": str(recipe["_id"]),
"name": recipe["name"],
"ingredients": recipe["ingredients"]
}

CRUD functions

The next step is to write the functions responsible for saving, removing, updating and deleting recipes. Start by implementing the save_recipe:

1
2
3
4
5
def save_recipe(recipe_data: dict) -> dict:
recipe = recipe_collection.insert_one(recipe_data).inserted_id
return {
"id": str(recipe)
}

The function above inserts the recipe data into the database and returns the newly created recipe’s ID.

Next, the function for retrieving a single recipe and all the recipes from the database:

1
2
3
4
5
6
7
8
9
10
11
def get_single_recipe(id: str) -> dict:
recipe = recipe_collection.find_one({"_id": ObjectId(id)})
if recipe:
return parse_recipe_data(recipe)

def get_all_recipes() -> list:
recipes = []
for recipe in recipe_collection.find():
recipes.append(parse_recipe_data(recipe))

return recipes

The first function above returns a single recipe whose ID matches the supplied one and an error message if it doesn’t exist, while the second function returns all the contained recipes in the database.

Next, write the update_recipe_data function responsible for updating recipe data:

1
2
3
4
5
def update_recipe_data(id: str, data: dict):
recipe = recipe_collection.find_one({"_id": ObjectId(id)})
if recipe:
recipe_collection.update_one({"_id": ObjectId(id)}, {"$set": data})
return True

Lastly, write the function for deleting a recipe:

1
2
3
4
5
def remove_recipe(id: str):
recipe = recipe_collection.find_one({"_id": ObjectId(id)})
if recipe:
recipe_collection.delete_one({"_id": ObjectId(id)})
return True

With the database CRUD functions in place, replace the content of app/api.py with:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
from fastapi import FastAPI, Body
from fastapi.encoders import jsonable_encoder

from app.model import RecipeSchema, UpdateRecipeSchema
from app.database import save_recipe, get_all_recipes, get_single_recipe, update_recipe_data, remove_recipe

app = FastAPI()

@app.get("/", tags=["Root"])
def get_root() -> dict:
return {
"message": "Welcome to the okteto's app.",
}

@app.get("/recipe", tags=["Recipe"])
def get_recipes() -> dict:
recipes = get_all_recipes()
return {
"data": recipes
}

@app.get("/recipe/{id}", tags=["Recipe"])
def get_recipe(id: str) -> dict:
recipe = get_single_recipe(id)
if recipe:
return {
"data": recipe
}
return {
"error": "No such recipe with ID {} exist".format(id)
}

@app.post("/recipe", tags=["Recipe"])
def add_recipe(recipe: RecipeSchema = Body(...)) -> dict:
new_recipe = save_recipe(recipe.dict())
return new_recipe

@app.put("/recipe", tags=["Recipe"])
def update_recipe(id: str, recipe_data: UpdateRecipeSchema) -> dict:
if not get_single_recipe(id):
return {
"error": "No such recipe exist"
}

update_recipe_data(id, recipe_data.dict())

return {
"message": "Recipe updated successfully."
}

@app.delete("/recipe/{id}", tags=["Recipe"])
def delete_recipe(id: str) -> dict:
if not get_single_recipe(id):
return {
"error": "Invalid ID passed"
}


remove_recipe(id)
return {
"message": "Recipe deleted successfully."
}

Update the api/model.py by removing the id field in the RecipeSchema model class:

1
2
3
4
5
6
7
8
9
10
11
class RecipeSchema(BaseModel):
name: str = Field(...)
ingredients: List[str] = Field(...)

class Config:
schema_extra = {
"example": {
"name": "Donuts",
"ingredients": ["Flour", "Milk", "Sugar", "Vegetable Oil"]
}
}

Testing The Database

With the database connection in place, start a mongod server to allow interactions with the database:

1
mongod --port 27017

Next, test the POST route:

1
2
3
(venv)$ curl -X POST http://localhost:8080/recipe -d \
'{"name": "Donut", "ingredients": ["Flour", "Milk", "Butter"]}' \
-H 'Content-Type: application/json'

Response:

1
2
3
{
"id": "601fdcd82fbbf462d33a6e34"
}

Test the GET routes:

  1. Return all recipes
1
(venv)$  curl -X GET http://localhost:8080/recipe/2 -H 'Content-Type: application/json'

Response:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"data": [
{
"id": "601fdcd82fbbf462d33a6e34",
"name": "Donut",
"ingredients": [
"Flour",
"Milk",
"Butter"
]
}
]
}
  1. Return a single recipe
1
(venv)$ curl -X GET http://localhost:8080/recipe/601fdcd82fbbf462d33a6e34 -H 'Content-Type: application/json'

Response:

1
2
3
4
5
6
7
8
9
10
11
{
"data": {
"id": "601fdcd82fbbf462d33a6e34",
"name": "Donut",
"ingredients": [
"Flour",
"Milk",
"Butter"
]
}
}

Test the UPDATE route:

1
(venv)$ curl -X PUT "http://0.0.0.0:8080/recipe?id=601fdcd82fbbf462d33a6e34" -H  "accept: application/json" -H  "Content-Type: application/json" -d "{\"name\":\"Buns\",\"ingredients\":[\"Flour\",\"Milk\",\"Sugar\",\"Vegetable Oil\"]}"

Response:

1
2
3
{
"message": "Recipe updated successfully."
}

Lastly, test the DELETE route:

1
(venv)$ curl -X DELETE "http://0.0.0.0:8080/recipe/601fdcd82fbbf462d33a6e34" -H  "accept: application/json"

Response:

1
2
3
{
"message": "Recipe deleted successfully."
}

Redeploying to Okteto

Okteto eases the stress of deployment and subsequent redeployment by allowing us to update and upgrade existing applications from the stack file. In the previous post, we created an okteto stack manifest to deploy our fastAPI service. We are now going to update it also include a mongodb instance as part of the deployment:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
services:
fastapi:
build: .
ports:
- 8080:8080
environment:
- DB_HOST=mongodb://mongodb:27017
- secret=dev
mongodb:
image: bitnami/mongodb:latest
ports:
- 27017
volumes:
- data:/bitnami/mongodb
volumes:
data:

In the code above, you added another service, mongodb, to house a MongoDB container from bitnami. It’s configured to expose MongoDB’s default port 27017 is exposed. This container will only be accessible from your namespace, and it’s configured with a persistent volume /bitnami/mongodb to ensure that data can be retrieved when the application restarts.

Under the fastapi service, add an environment heading containing the DB_HOST the database file reads using the decouple library:

1
2
3
environment:
- DB_HOST=mongodb://mongodb:27017
- secret=dev

With the stack file updated, deploy it using the command:

1
(venv)$ okteto stack deploy --build

Log on to your Okteto Dashboard. Notice that your application now includes an instance of MongoDB alongside your application:

Dashboard

Test the recipe route by replacing deployedapp from previous requests with the live application URL. From your terminal, run the command:

1
(venv)$ curl -X POST "https://fastapi-youngestdev.cloud.okteto.net/recipe" -H  "accept: application/json" -H  "Content-Type: application/json" -d "{\"name\":\"Donuts\",\"ingredients\":[\"Flour\",\"Milk\",\"Sugar\",\"Vegetable Oil\"]}"

The response sent out is:

1
2
3
{
"id": "601fe6deaa1a27fbcb9a60fb"
}

Committing Changes to Git

With the changes confirmed and tested, commit all the changes in the application to the branch:

1
2
git add .
git commit -m "Added MongoDB to the recipe application"

Push the committed changes:

1
git push -u origin mongo-crud

Conclusion

In this article, you modified your fastAPI service to use a real database. You then added the database service to your okteto stack manifest and deployed the changes with one command. Finally, you tested the changes end to end, ensuring that they work as expected. The final version of the code is available on our Github repository.

Create your free Okteto account today and begin developing your new application with one click.