Back Story

I am building a library to manage content in apps (blog pages, help text, task instructions) based on TinyMCE (more on that in an upcoming post). The content will include images that the user can upload.

Content Image Business Object

I created a Context Image business object with properties for name, type, size and base64Content. I also added a hash property which will be key to the caching strategy that I use.

I made the Content Image business object a child of the Content business objects and set the delete behavior to clean up the images if the content is deleted.

Update Hash Behavior

I added an Update Hash behavior to the Content Image business object. It is a "Before Save" event handler. That means it is called before create and before update. If there is base64Content, it uses the Node.js crypto module to calculate a hash and add or update it on the content image. It also sets the image size in case the caller forgot to do that.

From Hash Behavior

So why are we doing all this hash stuff? First, we don't want to depend on the user assigning unique names to images so we will use the hash as the key to retrieve the image. Second, we want to cache the images on the client's machine and not reload it if it has not changed.

To accomplish this, I added a From Hash behavior to the Content Image business object. It is a "Get" class behavior (so that the url does not need the id) that has hash as a parameter.

Index on Hash

Since we will be retrieving images by their hash, I added an index to the Content Image business object.

Open Access to Endpoint

This endpoint will be used in the html of the content with something like this:

<img src="/api/ContentImages/fromHash?hash=6d22ea84fd5f6a7dfc0f79da33dfc56f10b956d5">

This would result in a 403 unauthorized error. There are two reasons. First, the default access rules on a behavior come from the business object itself and in this case, users have to be logged in to access the business object. Second, because the browser does not add the authorization header automatically, access to the endpoint is denied.

You could add an Http Interceptor to your client app that would add the authorization header to image requests if you only wanted users that are authenticated to access the images.

In our case, we want everyone to have access to the images regardless of whether they are authenticated or not so we just add an access rule to the behavior.

Accessing the Response Object

The server side component of an Apex Designer app is a Loopback 4 application. The "options" parameter is added to all behaviors and is used to pass context down through the call stack. Behaviors that are exposed as a REST endpoint receive the Express.js request and response objects as properties in the options parameter. That is important for this behavior because we want to respond in a very specific way - not just return data.

Behavior Logic Highlights

First, we attempt to find the content image using its hash.

let contentImage = await ContentImage.findOne({ where: { hash: hash } }, options);

If we find a contentImage with that hash, we set the status code to 200 and add two headers. The Content-Type header contains the mime type of the image. The Cache-Control header tells the client to hold onto the image indefinitely.

options.response.writeHead(200, {
	'Content-Type': contentImage.type,
	'Cache-Control': 'max-age=315360000'

Finally, we convert the base64Content to a Buffer and send and end the response with that.

options.response.end(Buffer.from(contentImage.base64Content, 'base64'));

If the caller did not send a hash or we did not find a content image with that hash, we respond with a 404 not found.

options.response.status(404).send('Not Found');

The Results

The Chrome Network tab shows that the Content-Type and Cache-Control headers were set correctly.

It also shows that on the second load of the page, the images were retrieved from cache correctly.


Apex Designer delivers great flexibility and power in a low-code design experience. Want to learn more? Visit and sign up for a demonstration. I would love to show you how easy it is to build apps that run on an open source stack.