go back

Placeholder for post cover image
Cover image for post

Building APOD color search part III: Deno Search API 🔎

April 21, 2023

If you're following along, this is the third part in a series about how I built 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 and continuously deployed using Deno 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 as it provides some basic routing and scaffolding. There's a helpful supabase Deno package 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:

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" },
Enter fullscreen mode Exit fullscreen mode

Great, now to return results generated via the image analysis from before! (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:

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,
Enter fullscreen mode Exit fullscreen mode

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:

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

const { data: colors } = await supabase
  .gt("r", r - THRESHOLD)
  .lt("r", r + THRESHOLD)
  .gt("g", g - THRESHOLD)
  .lt("g", g + THRESHOLD)
  .gt("b", b - THRESHOLD)
  .lt("b", b + THRESHOLD);
Enter fullscreen mode Exit fullscreen mode

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:

const { data: clusters } = await supabase
  .in("color_id", colors?.map(({ id }) => id))

clusters.sort((a, b) => minimizeDistance(a, b));
Enter fullscreen mode Exit fullscreen mode

The source for the minimizeDistance function above can be found in utils.ts. Finally, once the most relevant clusters are found we can return the actual APODs:

const { data } = await supabase
  .in("id", clusters?.map(({ day_id }) => day_id))

return new Response(JSON.stringify({ data }), {
  status: 200,
  headers: { "Content-Type": "application/json" },
Enter fullscreen mode Exit fullscreen mode

Success! This API is deployed here 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 before each hex and color/cluster query, using Redis Enterprise Cloud's free tier.

go back