| <!-- -->Christopher Leggett's Portfolio and Blog<!-- -->

Golang REST API Tip - Use an Embed Parameter to Embed Related Resources

July 23, 2021

In a REST API, you often have relations between resources. While you can certainly return IDs for these related objects and require the API consumer to query them, that can be somewhat inconvenient for your users. On the other hand, you certainly don't want to eagerly load these on every request, or you could be sending unnecessarily large payloads in your responses. So what can you do?

In my case, I chose to support a query parameter with the name embed. Semantically, the logic is that we are "embedding" the related resource in the response for the resource requested by the path. For example, in a bookmark management app, you may be requesting a folder and embed the bookmarks in the folder with the following URL.

<baseUrl>/folders/<id>/?embed=bookmarks

With that, you would receive a response body similar to the following:

{
    "id": 1,
    "name": "folder",
    "bookmarks": [
        {
            "id": 1,
            "name": "My Website",
            "url": "https://chrisleggett.me"
        },
        {
            "id": 2,
            "name": "Devmarks Demo",
            "url": "https://demo.devmarks.app"
        }
    ]
}

Side Note: I picked the word "embed" here, but there is no standard around this. It would be just as valid for you to choose "preload" or "include". As long as your choice is documented with the rest of your api documentation, it ultimately doesn't matter.

Now how would you actually implement this? Below I will provide some code excerpts that show how I chose to implement this. My examples below use gorilla/mux for routing, and gorm for database access via an ORM, but with some light modification this concept should be able to be implemented with other libraries as well. Additionally, I do not claim that I am the first to come up with this, nor do I claim that this is the only way or even the best way to accomplish this.

First off, I recommend extracting the embed query parameter from the URL and saving it in the context via middleware so it can be pulled out of the context later in any method down the line. As such, we need to setup a key for the context to use for storing and retrieving the embed values. I define this in a separate helpers package in order to avoid cyclical imports.

// helpers/ctx.go
package helpers

type contextKey struct {
	key string
}
var EmbedsKey = contextKey{"embeds"}

// Wherever you choose to define this middleware
...
import <project_url>/helpers
...
func apiMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		values := r.URL.Query()
		embeds := strings.Split(values.Get("embed"), ",")
		ctx := r.Context()
		ctx = context.WithValue(ctx, helpers.EmbedsKey, embeds)
		next.ServeHTTP(w, r.WithContext(ctx))
		
	})
}

As you can see, the helpers package defines a contextKey struct and defines an exported instance of that key called EmbedsKey. Wherever we define our middleware, we then import the helpers package and use the EmbedsKey to save an array of strings that we extracted from the embeds query parameter. We then move onto our next middleware/final handler passing in the ResponseWriter and the Request object with our modifed context inside.

Make sure you use the middleware in your router.

import 	"github.com/gorilla/mux"

r := mux.NewRouter()
r.Use(apiMiddleware)
// your routes and handlers here

Next, your request goes through your handler and eventually makes it to your database code. Make sure your handler function extracts the context from the Request (ctx := r.Context()) and then passes it down to your database handling package. This is where I extract the embeds array we saved in the context earlier. GORM has a Preload function that we can use to preload associations defined in your models. However, you need some way to validate the embed values to make sure they are valid associations in your database, as there's nothing stopping an api consumer from putting anything at all in the embeds value.

To start off with, let's wrap the logic around setting up the *gorm.DB instance with the preloads.

package db

type Database struct {
	*gorm.DB
}

// You should probably put this in your helpers package,
// I have it here for clarity.
func contains(array []string, s string) bool {
	for _, x := range array {
		if x == s {
			return true
		}
	}
	return false
}

func (db *Database) preloadEmbeds(valid []string, embeds []string) *gorm.DB {
	var instance = db.DB
	for _, embed := range embeds {
		if contains(valid, embed) {
			instance = instance.Preload(strings.Title(embed))
		}
	}
	return instance
}

This function takes an array of valid embed values and the ones that were provided in the request. It then returns an instance of *gorm.DB with the Preloads prepared. It currently just ignores any invalid embed values that are provided. If no embed values are provided, then it simply returns the same *gorm.DB instance it was called on. As this returns a *gorm.DB, we can immediately chain a First() or Find() method, for instance, off of this function. For example:

// db/folder.go
func (db *Database) GetFolderByID(ctx context.Context, id uint) (*model.Folder, error) {
	var folder model.Folder
	embeds, ok := ctx.Value(helpers.EmbedsKey).([]string)
	if !ok {
		return nil, errors.New("embeds parsing error")
	}
	return &folder, errors.Wrap(db.preloadEmbeds(model.FolderValidEmbeds(), embeds).
        First(&folder, id).Error, "unable to get folder")
}

// model/folder.go
package model

type Folder struct {
    ID    uint `json:"id"`
	Name  string `json:"name"`

	Bookmarks []Bookmark `gorm:"many2many:bookmark_folder;" json:"bookmarks"`
}

// Add strings to the array to allow embedding that resource through the
// embed query paramter.
func FolderValidEmbeds() []string {
	return []string{"bookmarks"}
}

Unfortunately Go does not have a good equivalent to static methods, so the next best thing is to define a namespaced exported function in the models package that returns an array of strings which represent the list of valid embed values. I named this one FolderValidEmbeds. If later on we develop a relationship between Bookmarks and Tags and we want to provide embed support for it, for example, you would need to define a similar function called BookmarkValidEmbeds that returns an array of strings containing "tags".

At this point, your API handler should write out the returned object as json to your api consumer. If you wish to see this in practice, check out my open source bookmark manager api. It is still in active development, but besides some minor reorganization this idea probably won't change. Check it out here!