DEV Community

Damilare Agba
Damilare Agba

Posted on

Building a CMS API with Fauna and FastAPI

During my recent journey on working with databases, I found a really interesting database to work with. In the past, I have done major work with MongoDB(a NoSQL database) and many other relational databases and I was looking to work with something relatively different.

Today we will build a simple content management system API leveraging fauna and FastAPI, a python framework for building relatively fast APIs.

What is Fauna

Before we get started, let us take a look at a major tool in our arsenal today; Fauna.

Fauna is an interface for interacting with your data that has been stored in a serverless database. The engine on which it runs provides a "...fast, consistent, and reliable..." system, "... with a modern security infrastructure."

Fauna can be seen as a hybrid database system "NoSQL system's flexibility and with the relational querying and transactional capabilities of SQL databases."

Now lets talk about serverless databases. A serverless database is one which gives room for developers to host their data without the need to worry about the low-level specifics of the system hosting it. This allows the developer to spend more time on the development of the application itself.

Setting up our fauna database

To get started, we need to install the fauna driver to be able to make use of it in our project.

$ pip install faunadb
Enter fullscreen mode Exit fullscreen mode

After installing the we want to start out our project so we'll be creating our main.py file which would contain all the logic and resources we'll be exposing on our API.

To create our file we can run this in the terminal

$ touch main.py
Enter fullscreen mode Exit fullscreen mode

Before we get started with writing our code, we need to first create our database on the fauna dashboard.

![Image description]

After creating our database, we need to create our server key. We do this by going to the security tab on our dashboard and creating a new key. admin
Make sure the role is server*

![Image description]

We would get a key which we need to store somewhere safe as fauna would only give us the key once.

Now we can proceed to writing our code. Let us go to our main.py file.

Connecting to our fauna database

We would get started by connecting to our fauna database so we can interact with it.

To do that, first we import the modules need to connect to our db

from faunadb import query as q
from faunadb.objects import Ref
from faunadb.client import FaunaClient
Enter fullscreen mode Exit fullscreen mode

Then we can create our server client by doing

serverClient = FaunaClient(secret="YOUR_FAUNADB_ADMIN_SECRET")
Enter fullscreen mode Exit fullscreen mode

The server client is what is needed to interact with the Fauna data-API (our database).

Our secret here is the server key we generated earlier.

After we create our server client, we proceed by creating our collection. In lay-man terms, a collection is a group of documents* with almost specific similarities, e.g a collection of users, a group of students, and the like. From the perspective of relational databases, it can be seen as a table

*We'll get to that later

We'll be creating two collections, one for our users and the other for our posts. A collection can be created with our client.

serverClient.query(q.create_collection({"name": "users"}))

serverClient.query(q.create_collection({"name": "posts"}))
Enter fullscreen mode Exit fullscreen mode

Next we get ahead by creating indexes for each collection. An index is a way of retrieving documents from our collection. Indexes are needed to be able to make queries to the collections.

Let us create our index for the users and posts

serverClient.query(q.create_index(
    {
        "name": "users_by_email",
        "source": q.collection("users"),
        "permissions": {"read": "public"},
        "terms": [{"field": ["data", "email"]}],
        "unique": True
    }
))

serverClient.query(q.create_index(
    {
        "name": "posts_by_author",
        "s_[](url)_ource": q.collection("posts"),
        "terms": [{"field": ["data", "author"]}],
    }
))

Enter fullscreen mode Exit fullscreen mode

Now before moving forward, let us address what a document is. Writing data to a collection creates a document which can be seen as an individual row of data. From the point of view of relational db a document is a row in a table.

Now that we have all these ready, let us proceed to writing our code.

We would be leveraging FastAPI, a python web framework to create our content management system.

To get started, we would create our app which we would be building our endpoints on.
First, let us import all the libraries we would be using

from datetime import datetime, timedelta
from fastapi import FastAPI, Path, Query, Header


# this creates our app
app = FastAPI()
Enter fullscreen mode Exit fullscreen mode

It is upon this app that we would build our endpoints and logic.

We would be looking at the basic CRUD functionalities of fauna in this article. You can find the code for the project on github.

To get started, let us create a register route for our app. Here we would look at creating and reading a document here. Before we do that, we need to create a model for the information we want our endpoint to receive. This servers as a guide for the data each endpoint receiving information wants.

In FastAPI, pydantic abstracts all the complexities and gives us a simple way to do this.

from pydantic import BaseModel

class User(BaseModel):
    name: str
    email: str
    password: str
Enter fullscreen mode Exit fullscreen mode

This serves as a base model for our user class. We would see how this is used soon.

Let us proceed by creating a way for our users to register. Before that we also need to import some additional libraries. We would import all the libraries we need here and now then we start hacking.

import uuid
from faunadb import query as q
from faunadb.objects import Ref
from datetime import datetime, timedelta
from fastapi import FastAPI


# we create our register route
@app.post('/register')
def register(user: User):  
    pass

Enter fullscreen mode Exit fullscreen mode

Here, we defined that our route would be a post request. Recall the base model we created earlier, we are making use of it here note the data we are expecting with the post request (for the user, we want the name(a string), password (a string) and email (also a string). Recall that FastAPI generates a documentation for us and this would also be included in the documentation automatically.

Then let's proceed to write our logic. What we do is

  • First check if the email provided has been used by another user (here we'll read from our db using the index we created)
  • If it hasn't, we create a new user (write to our db)
@app.post('/register')
def register(user: User):
    try:
        # here we read from our db using our index
        sc.query(q.get(q.match(q.index("users_by_email"), user.email)))
        return {
            "msg": "error",
            "detail": "Email has been used."
        }
    except:
        # write the user info to our db
        sc.query(q.create(q.collection('users'), {
            'data': {
                "email": user.email,
                "name": user.name,
                "password": hash_password(user.password),
                "pid": str(uuid.uuid4())
            }
        }))
        return {
            "msg": "success",
            "detail": "Created Successfully"
        }
Enter fullscreen mode Exit fullscreen mode

With this simple resource, we've seen the create and read feature of fauna.

No let us proceed to updating our user information. Earlier, we noticed that we used a base model to receive our user information. However, we can't use the same model now because all fields in the model are compulsory and for our update resource, we might only want to update one field.

To fix this, let us simply create a new base model and make the fields optional.

class UpdateUser(BaseModel):
    name: Optional[str] = None
    email: Optional[str] = None
    password: Optional[str] = None
Enter fullscreen mode Exit fullscreen mode

Now we can proceed to creating our update user resource

@app.post("/update-user")
def update_user(user: UpdateUser, x_access_token: str = Header(None)):
    try:
        data = jwt.decode(x_access_token, secret_key,
                          algorithms=['HS256'])
    except Exception:
        return {
            'msg': 'error',
            'details': 'Token is invalid'
        }
    try:
        resp = sc.query(q.get(q.match(q.index("users_by_email"), data['email'])))
    except:
        return {
            'msg': 'error',
            'details': 'User not found'
        }
    uid = resp['ref'].id()
    data = {}
    if user.email is not None:
        data["email"] = user.email
    if user.name is not None:
        data["name"] = user.name
    if user.password is not None:
        data["password"] = hash_password(user.password)

    sc.query(q.update(q.ref(q.collection("users"), uid), {"data": data}))
    return {
        "msg": "success",
        "details": "User updated successfully"
    }
Enter fullscreen mode Exit fullscreen mode

Here, we see how to update our document in the user collection with the update query.
We have seen ways of interacting with our fauna collection. The syntax used about is a generic syntax for python so it can be used outside FastAPI.

I hope we have learn a few things from this short article.
Cheers

Top comments (0)