Skip to content

Discussion #1

Best Practices for Brev Storage

The Brev DB is a simple key value store. It's a nosql database, but even if you're using sql, these best practices are relevant.

Let's build a simple API to get and create users. Create an endpoint and make sure you have a GET call for fetching the users, and a POST call for creating the users.

import variables
import shared


def get():
  return {}

def post():
  return {}

Add the database. We'll just name this table "Users". Don't forget to pass the db in to both of your functions.

import variables
import shared
from global_storage import storage_context


def get(db = storage_context("Users")):
  return {}

def post(db = storage_context("Users")):
  return {}

We want to create the user via a POST call and an incoming JSON object. We can create a pydantic class to represent the expected incoming data, and simply pass it in to our post call.

import variables
import shared
from global_storage import storage_context
from pydantic import BaseModel

class User(BaseModel):
  phone: str
  name: str
  create_date: str


def get(db = storage_context("Users")):
  return {}

def post(postUser: PostUserSchema, db = storage_context("Users")):
  return {f"Welcome {user.name}"}

Below is a simple implementation of using the db to add users.

import variables
import shared
from global_storage import storage_context
from pydantic import BaseModel

class User(BaseModel):
  phone: str
  name: str
  create_date: str


def get(db = storage_context("Users")):
  return {}

def post(postUser: PostUserSchema, db = storage_context("Users")):
  db[postUser.phone] = postUser.dict()
  return {f"Welcome {user.name}"}

It's a simple way that absolutely works! We're conveniently using the phone number as the unique key, which is a pretty safe assumption.

There are a couple drawbacks with this method:

  1. We might want different fields for different transactions. For example, create_date should be generated on the server, not on the incoming json.
  2. The creation logic should be separated from the storing logic, so that if creating requires custom logic, you won't have to replicate it elsewhere.
  3. We should isolate database operations from the in-memory object. This will allow your code to require less refactoring if you change the underlying database.

To start, let's add a function to create the user. In our case this is trivial, but it could include some more complicated logic later, like triggering some downstream effect. This function should be part of the pydantic class. Since we want this function to create an instance of the class, not operate on an instance, we will use a class method.

import variables
import shared
from global_storage import storage_context
from pydantic import BaseModel
import datetime

class User(BaseModel):
  phone: str
  name: str
  create_date: str

  @classmethod
  def create(cls, name, phone):
    return cls(name=name, phone=phone, create_date=datetime.datetime.now().isoformat())

def get(db = storage_context("Users")):
  return {}

def post(postUser: PostUserSchema, db = storage_context("Users")):
  db["id"] = postUser.dict()
  return {f"Welcome {user.name}"}

Two things to notice:

  1. The create_date is now generated by the create function, not from incoming json.
  2. The instance is created, but nothing is persisted in the database.

There are use cases where not persisting in the database is desirable. Say you have a logic flow of first creating the user, then verifying if they have a specific attribute, which without one, you want to abort. Rather than always persisting the user then having to clean up if it was a mistake, now you have to be intentional about persisting the user. Let's go ahead and add the persisting function.

import variables
import shared
from global_storage import storage_context
from pydantic import BaseModel
import datetime

class User(BaseModel):
  phone: str
  name: str
  create_date: str

  @classmethod
  def create(cls, name, phone):
    return cls(name=name, phone=phone, create_date=datetime.datetime.now().isoformat())

  def add(self, db):
    db[self.phone] = self.dict()
    return self.dict()

def get(db = storage_context("Users")):
  return {}

def post(postUser: PostUserSchema, db = storage_context("Users")):
  db["id"] = postUser.dict()
  return {f"Welcome {user.name}"}

Few things to notice.

  1. Unlike the create function, we need an instance of the user to persist in the database, so the add function is not a classmethod. It will only operate on a user.
  2. It requires you to pass in the instance of the database you want to add.
  3. .dict() is a built in pydantic function that converts the user object to a dictionary before persisting.

Notice how we are no longer using the create_date being passed in, and generating it on creation instead. We should modify our post call so it doesn't require the extraneous field. We changed the name of the original BaseModel class, so we actually don't have to modify the class we pass in to the POST call.

import variables
import shared
from global_storage import storage_context
from pydantic import BaseModel
import datetime

class User(BaseModel):
  phone: str
  name: str
  create_date: str

  @classmethod
  def create(cls, name, phone):
    return cls(name=name, phone=phone, create_date=datetime.datetime.now().isoformat())

  def add(self, db):
    db[self.phone] = self.dict()
    return self.dict()

class PostUserSchema(BaseModel):
  phone: str
  name: str


def get(db = storage_context("Users")):
  return {}

def post(postUser: PostUserSchema, db = storage_context("Users")):
  db["id"] = postUser.dict()
  return {f"Welcome {user.name}"}

Now let's use our create and add function!

import variables
import shared
from global_storage import storage_context
from pydantic import BaseModel
import datetime

class User(BaseModel):
  phone: str
  name: str
  create_date: str

  @classmethod
  def create(cls, name, phone):
    return cls(name=name, phone=phone, create_date=datetime.datetime.now().isoformat())

  def add(self, db):
    db[self.phone] = self.dict()
    return self.dict()

class PostUserSchema(BaseModel):
  phone: str
  name: str


def get(db = storage_context("Users")):
  return {}

def post(postUser: PostUserSchema, db = storage_context("Users")):
  user = User.create(**postUser.dict())
  user.add(db)
  return {f"Welcome {user.name}"}

Yay! It works! Now we just need a function to get the users for our GET request. This function will also be a classmethod, since you should be able to get all the users without needing an instance of user.

import variables
import shared
from global_storage import storage_context
from pydantic import BaseModel
import datetime

class User(BaseModel):
  phone: str
  name: str
  create_date: str

  @classmethod
  def create(cls, name, phone):
    return cls(name=name, phone=phone, create_date=datetime.datetime.now().isoformat())

  @classmethod
  def getAll(cls,db):
    return db.items()

  def add(self, db):
    db[self.phone] = self.dict()
    return self.dict()

class PostUserSchema(BaseModel):
  phone: str
  name: str


def get(db = storage_context("Users")):
  return User.getAll(db)

def post(postUser: PostUserSchema, db = storage_context("Users")):
  user = User.create(**postUser.dict())
  user.add(db)
  return {f"Welcome {user.name}"}

And that's it! You might not want to implement everything here. For example, you might just want to have separate models for posting up data and for storing-- that way you can have server generated fields such as create_date. We'd love to continue this discussion. Reach us on slack or text Nader at 415-818-0207