Gallerify - A Fullstack Image Sharing Application Built On Netlify Primitives
Tags: netlifychallenge, devchallenge, webdev, javascript
Published: 2024-05-13 01:46:49

*This is a submission for the [Netlify Dynamic Site Challenge](https://dev.to/challenges/netlify): Visual Feast.*

*This is a submission for the [Netlify Dynamic Site Challenge](https://dev.to/challenges/netlify): Build with Blobs.*

*This is a submission for the [Netlify Dynamic Site Challenge](https://dev.to/challenges/netlify): Clever Caching.*


## What I Built
I developed a full-stack image sharing platform similar to [Giphy](https://giphy.com/) or [Imgur](https://imgur.com/) in order to showcase the power of building full-stack applications with only [Netlify Primitives](https://docs.netlify.com/platform/primitives/)

**So what are Netlify Primitives?**
Netlify Platform Primitives are a set of features provided by Netlify that can be used to build and enhance web applications. They are framework-agnostic, meaning they can be used with any web development framework you choose.

Most full-stack applications basically require a frontend, backend, database, authentication mechanism, caching and hosting.
Netlify provides services for all of the requirements above.
Gallerify is hosted on Netlify and leverages the Netlify Platform Primitives such as [Netlify Serverless Functions](https://docs.netlify.com/platform/primitives/#functions), [Netlify Blobs](https://docs.netlify.com/platform/primitives/#blobs) and [Netlify Image CDN](https://docs.netlify.com/platform/primitives/#image-cdn)
For authentication [Netlify Identity](https://docs.netlify.com/security/secure-access-to-sites/identity/) was leveraged.

**Gallerify Tech Stack**
**_Frontend:_** Next.js with Bootstrap 5 customized with SASS
**_Backend:_** Netlify Functions
**_Image Storage:_** Blob Data
**_Database Storage For Image Metadata:_** Blob Metadata
**_Authentication:_** Custom Netlify Identity in Netlify Functions

Even though Gallerify was built during the [Netlify Dynamic Site Challenge](https://dev.to/challenges/netlify) the goal was not to build the next Giphy or Imgur. 
It was tempting to build most of the features of Giphy or Imgur in order to be considered for the first spot. 
But later I realized that just focusing on the final product will not allow me to shine.

Instead of building most of the features of Giphy or Imgur I would rather focus on the basics by detailing my creative journey and then explain the choices I made, the challenges I faced, and how I overcame them.
Doing this will enable me to share an engaging and relatable content to the community.
Who knows? I might inspire someone to rather build the next Giphy or Imgur on Netlify Primitives.

If you got to this point, I encourage you to continue reading to learn how I built Gallerify.

## Demo
Before I get to the technical details, you can checkout Gallerify by clicking the link below. You can register for a new account and it's free or you can use the demo credentials below as well.

**Demo Credentials:**
**_Email:_** demo@demo.com
**_Password:_** demo1234

**Demo URL:** [https://gallerify-app.netlify.app](https://gallerify-app.netlify.app/)

## Screenshots:
**_Homepage:_**
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kfmpwrzo97epzdih5djl.png)

**_Layout Options:_**
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ns73315t86yl65x7esg6.png)

**_Transform Options_:**
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mbo9a4e5cx8jd9148ori.png)

**_Login:_**
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q7gdoekmappk1ugbrkl1.png)

**_Sign Up:_**
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/939w327dc3ujepmimn3a.png)

**_User Dashboard:_**
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tis168eb4dm95ugvtx6w.png)

**_Image Upload:_**
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/exnnxgtlj4ugs3itabvz.png)

## Platform Primitives

### Netlify Image CDN
For caching of images I leveraged [Netlify Image CDN](https://docs.netlify.com/platform/primitives/#image-cdn). 

To transform an image, you make a request to `/.netlify/images` of your site with query parameters that specify the source asset and the desired transformations
For the Masonry layout I render the cached version of the image without any transformations.

**Example URL:**
**_Mansory Layout Images:_**
`https://gallerify-api.netlify.app/.netlify/images?url=https://gallerify-api.netlify.app/api/gallery?uid=uploader_id_here&image_id=image_id_here&image_name=image_name_here`

**_Thumbnail Layout Images_**
`https://gallerify-api.netlify.app/.netlify/images?url=https://gallerify-api.netlify.app/api/gallery?uid=uploader_id_here&image_id=image_id_here&image_name=image_name_here&fit=cover&w=440&h=440`

For the thumbnail layout an optimized `440px x 440px` image is served by Netlify Image CDN on the fly.

The **url** query parameter is a Netlify functions endpoint that serves the image so that the **img** HTML tag can render the image.

Once you choose a particular layout the cached images are served. Also note that since my **url** endpoint also has query parameter I had to decode it using JavaScript function `encodeURIComponent` in order to prevent issues with the Netlify Image CDN if I also pass multiple query parameters.

Also once you see an image that you like and click on the image, an image transformation modal opens. In the image transformation modal, there are options to transform images.

Netlify Image CDN is leveraged to transform images on the fly once you type the values into the input, drag the sliders, and choose from the select inputs. 

Image transformations happens in realtime.
There are also buttons that set the image dimensions and other image options for popular social media sites. 
Once you click on a particular social media button, the image dimensions, image position, image fit, image quality, and image format values are automatically set and transforms in realtime.

Netlify Image CDN caches the images and serves the cached images upon requests. Therefore when an image that has been transformed is requested again, the cached version is served improving both the runtime performance and reliability of the Gallerify site.

**Challenges I Faced:**
When an image is added from the dashboard, it did not immediately appear on the homepage and dashboard.

**Decisions Made:**
To solve the problem of images not appearing immediately on the homepage and dashboard, I leveraged [Cache Control Settings](https://docs.netlify.com/platform/caching/#supported-cache-control-headers) in Netlify Serverless Functions response headers to revalidate and fetch a fresh copy of the images after 60 seconds of upload for the public homepage. For the dashboard, the control settings was set to 0. Images will appear immediately once they are uploaded

In a real world application, it's good practice to prevent your server from constant public requests. Thats why I set the revalidate option of 60 seconds for the homepage. Since the dashboard will not have as many requests like the homepage it's ideal to allow images to appear immediately so that users will not be confused about the images they uploaded.

```js
// Cache control headers for requests from dashboard

 return new Response(JSON.stringify(data_here), {
        headers: {
            "Content-Type": "application/json",
            "Cache-Control": "no-cache, no-store",
            "Netlify-CDN-Cache-Control": "no-cache, no-store",
           "Access-Control-Allow-Origin": "*"
        },
        status: statusCode 
    });
```

```js
// Cache control headers for requests from public frontend

 return new Response(JSON.stringify(data_here), {
        headers: {
            "Content-Type": "application/json",
            "Cache-Control": "public, max-age=60, must-revalidate",
            "Netlify-CDN-Cache-Control": "public, max-age=60, must-revalidate",
           "Access-Control-Allow-Origin": "*"
        },
        status: statusCode 
    });
```

### Netlify Identity
In order for users to login or register to your application, you need an authentication mechanism. 
I could have used Netlify Blobs to store user information and JWT tokens. However it's not ideal to be storing user credentials in blobs. Also since Netlify already provides authentication using Netlify Identity, it was better to leverage Netlify Identity as it makes authentication a breeze and then handles all authentication mechanisms out of the box.

Netlify provides [Netlify Identify](https://docs.netlify.com/security/secure-access-to-sites/identity/) in order to sign up, login, reset password, verify and confirm users.
In order to use Identity you can either use the [Netlify Identity Widget](https://github.com/netlify/netlify-identity-widget) or [GoTrue](https://github.com/netlify/gotrue) JavaScript plugin for custom configurations.

**Challenges I Faced:**
I wanted a custom authentication for Gallerify and I did not want to use the Default Identify widget as it will not match with the styling of my website.
Also even though the GoTrue plugin can help with customization I did not want authentication request to come from the frontend as an attacker can go through the GoTrue source code and then send malicious requests.

I wanted to use Netlify Identify in my Netlify Functions. At the time I did not know GoTrue was only for client-side so I installed it for my Netlify Functions only to be thrown multiple errors. I realized the package only works for the client-side

**Decisions Made:**
So solve the problem of using Netlify Identity in Netlify Functions I decided to go through the source code of the GoTrue project on GitHub and then learn how the requests were made. 

I figured out how requests were sent to Netlify Identify and then I wrote my custom authentication logic and then sent requests using fetch to login, sigup, and verify users JWT.

Once a request is sent the Netlify Function endpoint to create an account, a JWT token is returned and then stored in cookies on the Gallerify frontend for making authenticated requests by users.

I was able to customize my website and then make authentication requests from the frontend form to the Gallerify Netlify Function Backend without using the Identity Widget or GoTrue plugin.

```js

// Netlify Identity Signup

const registerResponse = await fetch(netlifyIdentitySignupUrl, {
                method: "POST",
                body: JSON.stringify({ email, password, data: { full_name: username } }),
                headers: { "Content-Type": "application/json"},
            });

const responsedata: any = await registerResponse.json();

```

```js

// Netlify Identity Login

const loginResponse = await fetch(netlifyIdentityLoginUrl, {
                method: 'POST',
                headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                body: `grant_type=password&username=${encodeURIComponent(
                email,
                )}&password=${encodeURIComponent(password)}`,
            });

const data: any = await loginResponse.json();

```

```js

// Netlify Identity Verify User - JWT

const headers = req.headers;

const accessToken = headers.get("authorization")?.split(" ")[1];

const verifyResponse = await fetch(netlifyIdentityVerifyUrl, {
            method: 'GET',
            headers: {
                "Authorization": `Bearer ${accessToken}`
            }
        });

const data = await verifyResponse.json();

```

### Netlify Blobs
For most applications, you need a location to store media. Gallerify is an image sharing platform so I needed a way to store the image files.
Netlify Blobs provides mechanisms to store the actual file data and then metadata or data that will be normally saved in a database.
I leveraged Netlify Blobs to store images as Blobs and authenticated user information that uploaded the file.

**Challenges I Faced:**

1. I did not have much knowledge on Netlify blobs so when I read the basics from the documentation and then I tried to save the image, anytime I try to serve the image to the frontend the image was served as a binary format.

2. Even though my Cache Control settings were previously set to a **max-age of 0** and then **no-cache**, any time there are updates and deletions my Netlify Function was still serving old data. Fresh data was only served after 60 seconds.

3. Listing Blobs using `await imagesStore.list()` does not return data with metadata but keys. I was using [for await...of](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of) to iterate through the response from `await images.list()`. 
Once the data increased my Netlify Functions were crashing due to the function execution duration of 10 seconds.

**Decisions Made:**

1. In order the solve the problem of images served as binary, I read multiple blog posts and articles. At some point I even used a third party image storage service until I stumbled upon @mattlewandowski93's post on this website.

I later figured out how to serve images properly by reading @mattlewandowski93's [submission post](https://dev.to/mattlewandowski93/netlify-image-game-1pk0). 

I want to say a big thanks to @mattlewandowski93's for explaining how he was able to serve the images properly from Netlify Functions.

@mattlewandowski93's post provided amazing insights on how he served images properly and how he implemented features for his application. 

I was able to create an API endpoint to properly serve images in the right format.

```js

// Code snippet of how to properly serve images

const userIdFromParams = url.searchParams.get("uid") as string;
 
const imageId = url.searchParams.get("image_id") as string;
const imageName = url.searchParams.get("image_name") as string;

const imageBlobKey = `${userIdFromParams}/${imageId}/${imageName}`;

const currentImageBlob = await imageStore.getWithMetadata(imageBlobKey, { type: "blob" });

 if (currentImageBlob) {

      return new Response(currentImageBlob.data, {
                    status: 200,
                    statusText: "OK",
                    headers: {
                        "Content-Type": currentImageBlob?.metadata?.type as any,
                    },
        });

 }

 return generateAppResponse("error", 404, "Image not found", currentImageBlob);
```

Also note that when you want to get blob data, you have to specify the **type**. In my case I used the **blob** type

```js
const currentImageBlob = await imageStore.getWithMetadata(imageBlobKey, { type: "blob" });
```

The line below is how the image is initially stored so that it can be sent back to the Netlify Functions to properly search based on prefixes. Searching by directory prefix style is faster.

```js
const imageBlobKey = ${userIdFromParams}/${imageId}/${imageName}`;
``` 

In order serve fresh data when updates and deletions occurred, I had to read the Netlify Blobs Documentation multiple times in order to understand the API better.

I later found out that Netlify Blobs uses an eventual consistency model by default. The data is stored in a single region and cached at the edge for fast access across the globe. Therefore when a blob is added, it becomes globally available immediately. 

However updates and deletions were propagated to all edge locations within 60 seconds. <u>**_This means that it may take up to 60 seconds_**</u> for a fresh blob to be served after it has been updated or deleted.

In order to prevent this from happening and then serve fresh data, I had to reconfigure the default behavior of the Blobs API and opt-in to strong consistency with the Netlify Blobs API.
After configuring using the **strong** options when I fetched images after images were added or deleted I was receiving the fresh data.

```js
 const imageStore = getStore({
        name: "uploads",
        consistency: "strong",
        siteID,
        token,
    });
```

In order to get metadata of blobs when using the `await imageStore.list()`, I used the technique below.

```js
// Get metadata for each blob

const images = [];

for await (const blob of entry.blobs) {
  const metadata = await store.getMetadata(blob.key);
  images.push(metadata);
}

```
However using the technique above was not efficient because when data begins to increase, once this code runs in your Netlify Function, your function times out after 10 seconds and an error is thrown.
Another option was to use [Background Functions](https://docs.netlify.com/functions/background-functions/) but that was going to be an overkill. Also I needed to serve a response immediately when I fetch for all data.
I wished the Blobs API had an option to list blobs with metadata.

For 2 days I was thinking about how to solve the problem of fetching data without my functions timing out after 10 seconds.
So I read the Documentation once more and decided to use a prefix. The reasoning behind this decision is that when `await imagesStore.list()` is executed the data is returned fast enough regardless the size of the data after uploading multiple images and testing it out. 

The problem was that the return value was the **key** and the **etag** for each blob.

Therefore by storing blobs using the key format below, I can get data belonging to a particular user. Think of it like a directory structure on your computer.

```js
 
 // The uuidv4 package was used to generate unique id's for files and images

// Store file in the format - userId/imageId/filename

const imageBlobKey = `${userId}/${imageId}/${imageName}`;

await imageStore.set(imageBlobKey, imageFile, {
                metadata: imageMetadata
            });
```

An array of keys in the format above is sent to the frontend and then when I want to fetch images belonging to a particular user I fetch by the prefix. The response from the execution is very fast.

```js

const userIdFromParams = url.searchParams.get("uid") as string;
const imageId = url.searchParams.get("image_id") as string;
            
const imageName = url.searchParams.get("image_name") as string;

const imageBlobKey = `${userIdFromParams}/${imageId}/${imageName}`;

const currentImageBlob = await imageStore.getWithMetadata(imageBlobKey, { type: "blob" });

if (currentImageBlob) {

   return new Response(currentImageBlob.data, {
                 status: 200,
                 statusText: "OK",
                 headers: {
                 "Content-Type": currentImageBlob?.metadata?.type as any,
         },
});

```

Anytime I want to get image data or image metadata there are API endpoints that receive the userId, imageId, fileName as query parameters and then it's reconstructed again in the format `userId/imageId/filename` so that I can fetch by a specific user or any image.


That it, at this point you have the learned how you can leverage Netlify Primitives to build full-stack applications.

I am confident this post will inspire a lot of developers to build the applications like Giphy, Imgur, Or the _"next big app"..._

I want to add that in case you have any challenges finding information on the [Netlify Documentation](https://docs.netlify.com/), try out the [Netlify Bot](https://docs.netlify.com/ask-netlify). It's so awesome and it helped me narrow down information I was looking for.

The ideas are endless. For example a Blog application, Social Media application, Podcast application e.t.c

You can checkout projects I work on by visiting my GitHub repo [**here**](https://github.com/clarnx)

Thanks for taking the time out of your day to read this blog post.

Happy building with [Netlify](https://www.netlify.com).