Building APOD color search part III: Deno Search API 🔎
Tags: webdev, javascript, deno, showdev
Published: 2023-04-21 16:27:29

If you're following along, this is the third part in a series about how I built [APOD color search](https://bryce.io/apod-color-search/).

## Simple search API 🔎

This is the layer that powers a `/search` endpoint to check the cache for results from previous searches, and if there are none then query the prepopulated database and store in cache. It's written in [Deno](https://deno.land/) and continuously deployed using [Deno Deploy](https://deno.com/deploy).

## Why Deno? 🦕

I like Deno for a variety of reasons, but first and foremost: it's _fast_. Native TS support is also nice, and Deno Deploy is a quick (and free) hosting service. Overall, it feels great to work with and is perfect for smaller projects.

Although it's somewhat overkill for the project I decided to use [fresh](https://fresh.deno.dev/) as it provides some basic routing and scaffolding. There's a helpful [supabase Deno package](https://deno.land/x/supabase@1.3.1) as well that I used to query the database.

## Finding color matches 🎨

To simplify the `/search` endpoint, the query is included in the URL as a route parameter. This is practical because the only relevant query is a six character string, being a hex color (e.g. `/search/:hex`). 

In fresh, a dynamic route like this is created using the filename `[param].ts` under the directory that the route expects, and in a request the value will be added to context with this name. So in my case, this means a directory named `search` containing a file named `[hex].ts`.

Now let's add a simple handler to get the `hex` param from context and return an empty response:

```jsx
export const handler = async (
  _req: Request,
  ctx: HandlerContext,
): Promise<Response> => {
  const { hex } = ctx.params;

  ...

  const data = [];

  return new Response(JSON.stringify({ data }), {
    status: 200,
    headers: { "Content-Type": "application/json" },
  });
}
```

Great, now to return results generated via the [image analysis from before](https://dev.to/bryce/building-apod-color-search-part-i-image-analysis-in-rust-24a5)! (The data structures are described there and will make this part more clear.) 

First, we'll need a connection to the database. Assuming these credentials are stored in an `.env`:

```js
import "https://deno.land/x/dotenv@v3.2.0/load.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@1.35.4";

const supabase = createClient(
  Deno.env.get("SUPABASE_URL") as string,
  Deno.env.get("SUPABASE_PUBLIC_API_KEY") as string,
);
```

Then, once we convert the hex value back to RGB we can then perform a query stringing multiple filters on the `colors` table together to get colors within a certain threshold of the input color:

```js
const THRESHOLD = 30;
const [r, g, b] = hexToRgb(hex);

const { data: colors } = await supabase
  .from("colors")
  .select()
  .gt("r", r - THRESHOLD)
  .lt("r", r + THRESHOLD)
  .gt("g", g - THRESHOLD)
  .lt("g", g + THRESHOLD)
  .gt("b", b - THRESHOLD)
  .lt("b", b + THRESHOLD);
```

This will give us a (long) list of colors, but this enables fetching a list of `clusters` from these colors to be sorted by raw distance to the original search color by comparing red, green and blue color values:

```js
const { data: clusters } = await supabase
  .from("clusters")
  .select()
  .in("color_id", colors?.map(({ id }) => id))
  .limit(15);

clusters.sort((a, b) => minimizeDistance(a, b));
```

The source for the `minimizeDistance` function above can be found in [utils.ts](https://github.com/brycedorn/apod-color-search/blob/main/api/routes/utils.ts). Finally, once the most relevant clusters are found we can return the actual APODs:

```js
const { data } = await supabase
  .from("days")
  .select()
  .in("id", clusters?.map(({ day_id }) => day_id))
  .limit(5);

return new Response(JSON.stringify({ data }), {
  status: 200,
  headers: { "Content-Type": "application/json" },
});
```

Success! This API is deployed [here](https://acs-api.deno.dev/search/ff00dd) if you want to play with it. This is the project on Deno Deploy and is used by the deployed FE.

## Caching results 🗂️

As mentioned earlier, there's also a caching layer to prevent unnecessary/repeated requests from forcing expensive database queries. I added simple calls to [redis](https://deno.land/x/redis@v0.29.2) before each hex and `color`/`cluster` query, using [Redis Enterprise Cloud](https://redis.com/redis-enterprise-cloud/overview/)'s free tier.