Persisting data

Json serialization

We recommend persisting objects using a hybrid relational and nosql model where nosql data is stored as json blobs in a database. You can use different strategies such as pure-relational and you can also use an ORM. We have written a blog post on why we recommend using a hybrid approach Sql DB, NoSql Schema that you may find interesting.

In the file /internal/app/list/list.go lets prepare the List struct for persistence by adding json serialization annotations.

type List struct { ID string `json:"id"` Name string `json:"name"` Items []string `json:"items"` }

Create a table

Add the create table sql statement to the database migration that is performed at server startup. The migrations can be found at /internal/db/migrations/migrations.sql.

CREATE TABLE IF NOT EXISTS lists ( id TEXT PRIMARY KEY, blob JSONB NOT NULL, );

Database CRUD functions

Lets create a new file called /internal/app/list/list_database.go for the database CRUD functions.

mytodo |--internal | |--app | |--list | |--list.go | |--list_db.go

The database CRUD functions will take a context and transaction, and execute sql commands to perform the functions. Here is an example of /internal/app/list/list_database.go. Note that these functions are private.

package list import ( "context" "database/sql" "encoding/json" ) func createList(ctx context.Context, tx *sql.Tx, list *List) error { jsonb, err := json.Marshal(&list) if err != nil { return err } _, err = tx.ExecContext(ctx, ` INSERT INTO lists ( id, blob ) VALUES ( $1, $2 )`, list.ID, jsonb, ) return err } func getListByID(ctx context.Context, tx *sql.Tx, ID string) (*List, error) { var data []byte err := tx.QueryRowContext(ctx, ` SELECT blob FROM lists WHERE id = $1`, id).Scan(&data) if err != nil { return nil, err } var list List return &v, json.Unmarshal(data, &v) } func updateList(ctx context.Context, tx *sql.Tx, list *List) error { jsonb, err := json.Marshal(&list) if err != nil { return err } _, err = tx.ExecContext(ctx, ` UPDATE lists SET blob = $1 WHERE id = $2`, jsonb, list.ID) return err } func getLists(ctx context.Context, tx *sql.Tx, Name string) ([]List, error) { rows, err := tx.QueryContext(ctx, "SELECT blob FROM lists LIMIT 5000", Name) if err != nil { return nil, err } defer rows.Close() vs := []List{} for rows.Next() { var data []byte if err = rows.Scan(&data); err != nil { return nil, err } var v List if err = json.Unmarshal(data, &v); err != nil { return nil, err } vs = append(vs, v) } return vs, nil }

Update the API

Now we can update the API functions in /internal/app/list/list.go to call the database functions.

func NewList(ctx context.Context, tx *sql.Tx, name string) (*List, error) { l := &List{ ID: uuid.NewString(), Name: name, } err := createList(ctx, tx, l) // call the private persistence function if err != nil { return nil, err } return l, nil } func AddItem(ctx context.Context, tx *sql.Tx, listID string, item string) error { l, err := getListByID(ctx, tx, listID) if err != nil { return err } // Check if the list is full if len(l.Items) >= MaxItems { return fmt.Errorf("todo list is full") } l.Items = append(l.Items, item) err = updateList(ctx, tx, l) // call the private persistence function if err != nil { return err } return nil }

Copyright © 2025