Deno Fresh Responsive Images: Resize API
Tags: deno, javascript, webdev, webassembly
Published: 2023-04-26 12:47:15

---
title: "Deno Fresh Responsive Images: Resize API"
published: "true"
description: "Deno Fresh Responsive Images 🖼️ how you can use unpic with Deno Fresh to self-host your images with an Imgix compatible resizing API."
tags: "deno, javascript, webdev, wasm"
canonical_url: "https://rodneylab.com/deno-fresh-responsive-images/"
cover_image: "https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1awcsc5ey0du4i7ha5hs.png"
---

## 📸 Unpic, Image Hosting Services and Self-Hosting Images

I put together this Deno Fresh responsive images post to talk about an idea I had when I first heard about **Matt Kane&rsquo;s new unpic tool**.  The framework which first set the standard for a **modern image plugin** was probably Gatsby, and Matt Kane was one of the engineers that worked on their plugin.  The idea behind unpic is to provide a simple API for including responsive images in your web apps.  It is designed with image hosting services in mind.  A kind of **one tool to rule them all** as there are components for **Astro, React, Preact and Svelte**, and each works with the popular image hosting services.

One complaint I often hear about image hosting services relates to cost. In fact, I got hit with a **surprise image hosting bill** for one side project I was working on.  Since then, I have favoured **self-hosting images** for side projects.  My idea was to use the **convenient API** which unpic provides with self-hosting.  This saves you writing a lot of client code for handling responsive images. The image files can be included in the front end project or in a separate serverless API app.

### Proof of Concept

I got something working.  It&rsquo;s more of a proof of concept than a final working solution.  Because I wanted the resizing API to be serverless and run on different hosting services, I opted for using **Rust WASM** for doing the image resizing heavy lifting.  That is all working, though I am still looking at a solution for generating images in next-gen formats like **AVIF and WebP**. In this post I take you through what I have so far and some ideas for the missing part.

## 🧱 Imgix Image API

I mentioned earlier that unpic supports multiple image hosting services.  For using unpic it makes sense to have our own self-hosted image resizing API mimic an existing one.  The <a aria-label="Im jix Rendering API" href="https://docs.imgix.com/apis/rendering">Imgix Rendering API</a> seems a popular choice and is used by Prismic, Sanity and other tools, so that is what I opted for.

In the rest of this post, I share some code from a Deno Fresh app I built as a proof of concept.  It uses a Rust WASM module for the actual resizing.  We skip over the Rust code here.  Under the hood, we use the `image` crate for resizing, in case you are interested.  You can see the Rust code in the GitHub repo (link further down).

![Deno Fresh Responsive Images: Screen capture shows three images of a dinosaur at different sizes](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nagqd7dlsxbhbfr8js1c.png)

## Client Frontend Markup

Deno Fresh uses Preact client markup.  However, there are React and Svelte plugins which use the same API and will work in exactly the same way if you prefer a different framework.

```typescript
import { Image } from "npm:@unpic/preact";

export default function Home() {
  const imageHost = "http://localhost:8000"
  return (
      <main className="wrapper">
        <h1 className="heading">FRESH!</h1>
        <section class="images">
      <Image
          src={`${imageHost}/api/images/dinosaur-selfie.png`}
          layout="constrained"
          width={128}
          height={128}
          alt="A dinosaur posing for a selfie"
          cdn="imgix"
        />
        <Image
          src={`${imageHost}/api/images/dinosaur-selfie.png`}
          loading="eager"
          layout="constrained"
          width={256}
          height={256}
          alt="A dinosaur posing for a selfie"
          cdn="imgix"
        />
        <Image
          src={`${imageHost}/api/images/dinosaur-selfie.png`}
          layout="constrained"
          width={64}
          height={64}
          alt="A dinosaur posing for a selfie"
          cdn="imgix"
        />
        </section>
      </main>
  );
}
```
The most interesting parts here are:
- We import unpic `Image` component in line `1`. It replaces the HTML `img` in our markup.
- We will see below that `unpic` manipulates the `src` value.  To output the correct code, I needed to use an absolute URL, even though a relative one would be fine here if we were using an `img` tag.
- The `src` value is actually an API route. Here it is available within the same project, though there is nothing stopping you from separating out the API route into its own serverless app.


## 🖥️ Generated Image Markup

This is the HTML which unpic generated for the first image:

```html
<img
  alt="A dinosaur posing for a selfie"
  loading="lazy"
  decoding="async"
  sizes="(min-width: 128px) 128px, 100vw"
  style="
    object-fit: cover;
    max-width: 128px;
    max-height: 128px;
    aspect-ratio: 1;
    width: 100%;
  "
  srcset="
    http://localhost:8000/api/images/dinosaur-selfie.png?w=128&amp;h=128&amp;fit=min&amp;auto=format 128w,
    http://localhost:8000/api/images/dinosaur-selfie.png?w=256&amp;h=256&amp;fit=min&amp;auto=format 256w
  "
  src="http://localhost:8000/api/images/dinosaur-selfie.png?w=128&amp;h=128&amp;fit=min&amp;auto=format"
/>
```

This has most lot of the attributes <a href="https://twitter.com/addyosmani/status/1388031656355794945/photo/1">Addy Osmani recommends</a> you set on your images for a good user experience.  You might notice we are missing source sets for next-gen images.  In fact, there is a way we can feature detect if a browser supports next-gen formats.  I will get to this later!

Before we get into that, though, some important features here are:

- `aspect-ratio` is set: this helps to reduce Cumulative Layout Shift
- `loading="lazy"`: this is a good default, and Chrome, Firefox and Safari all support lazy loading now
- `srcset`: the responsive part which provides hints to the browser, so a mobile device should not end up wasting data downloading an image bigger than it needs

Notice these features come out of the box with unpic.  You would have to remember to include them manually if you were working with a vanilla `img` tag.

## Image Resize API Route

The generated image source URLs follow this pattern:

```plaintext
http://localhost:8000/api/images/dinosaur-selfie.png?w=256&h=256&amp;fit=min&auto=format
```

The pathname includes a filename for the image we need. Then the URL search parameters provide the width and height values which you would expect.  As well as those, we have `fit=min` a scaling/resizing parameter. Finally, `auto=format` indicates the API should use <a href="https://docs.imgix.com/apis/rendering/auto/auto#format">content negotiation</a> to determine the most suitable image format (returning a next-gen format when supported).

Using Deno Fresh, we can create a template API route.  This will listen on the routes matching `http://localhost:8000/api/images/[FILENAME]`.  Astro, Remix and SvelteKit have mechanisms for handling template parameters.  With Deno Fresh, we can create a handler file at `routes/api/images/[file].ts` to listen on these routes.

```typescript
import { HandlerContext } from "$fresh/server.ts";
 
export const handler = async (
  request: Request,
  context: HandlerContext
): Promise<Response> => {
  if (request.method === "GET") {
    const {
      params: { file },
    } = context;
    
    const { url } = request;
    const { searchParams } = new URL(url);
    const width = searchParams.get("w");
    const height = searchParams.get("h");
    const fit = searchParams.get("fit");
    // TRUNCATED...
```

Here we are just reading the image parameters from the request URL. 

### Finding the Requested Image

Next, we can map the request pathname to an image file within the project.  Since the file parameter in the pathname can be arbitrary, it is important to check there is actually a matching file within the project and to return a `404` error where there is not one.

```typescript
      // ...TRUNCATED
      let image_bytes: Uint8Array | null = null;
 
      try {
        image_bytes = await Deno.readFile(`./images/${file}`);
      } catch (error: unknown) {
        if (error instanceof Deno.errors.NotFound) {
          return new Response("Not found", {
            status: 404,
          });
        }
      }
      // TRUNCATED...
```

### Deno Fresh Responsive Images: Resizing

The resizing is done with WASM code.  The JavaScript sharp library is an alternative here, though I decided to use WASM, so the code could be deployed in more environments.  sharp is based on the libvips C library and there is a related wasm-vips project.  <a href="https://www.npmjs.com/package/wasm-vips">wasm-vips is still in the early stages of development</a> though.

```typescript
import { instantiate, resize_image } from "@/lib/rs_lib.generated.js";
 
export const handler = async (
  request: Request,
  context: HandlerContext
): Promise<Response> => {
	  // TRUNCATED...
 
        await instantiate();
         
        const resizeOptions = {
          width: Number.parseInt(width),
          height: Number.parseInt(height),
          ...(typeof fit === "string" ? { fit } : {}),
        };
        const {
          error,
          resized_image_bytes: resizedImageBytes,
          mime_type: mimeType,
        } = resize_image(image_bytes, resizeOptions);
```

The import in the first line is where we make the WASM module accessible to the handler.


### Deno Fresh Responsive Images: Response

Finally, we can return the image with caching headers.  When returning a `404` error, set the headers not to cache, just in case there is a temporary issue.

```typescript
return new Response(new Uint8Array(resizedImageBytes), {
  headers: {
    "Content-Type": mimeType,
    "Cache-Control": "public, max-age=31536000, immutable",
  },
});
```

## 🦀 Rust WASM Module

The `wasmbuild` Deno module makes creating WASM code in Deno fairly straightforward.  It is a wrapper for `wasm-pack` and you can just use that directly if not working in Deno.  In Deno however, just add `wasmbuild` to your `deno.json`:

```json
{
  "tasks": {
    "start": "deno run -A --watch=static/,routes/ dev.ts",
    "wasmbuild": "deno run -A https://deno.land/x/wasmbuild@0.10.4/main.ts"
  },
  "importMap": "./import_map.json",
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "preact"
  }
}
```

Then initialize the project and once your Rust code is ready, build the WASM module:

```shell
deno task wasmbuild new
deno task wasmbuild
```

## 🐘 Deno Fresh Responsive Images: What is Missing?

I mentioned earlier that there is no next-gen image generation here yet.  We can implement our own form of content negotiation by inspecting the `Accept` header on the request:

```typescript
import { instantiate, resize_image } from "@/lib/rs_lib.generated.js";

export const handler = async (
  request: Request,
  context: HandlerContext
): Promise<Response> => {
    const { headers } = request;
    console.log(headers.get("Accept"));
 
    // TRUNCATED...
```

A typical header value here would be:

```plaintext
"image/avif,image/webp,*/*"
```

That is for the current version of Firefox, letting us know we can send AVIF or WebP images in this case.  We can then request the preferred format from the WASM module, so it churns out a next-gen image when supported.

What is missing is the WASM code to output the next-gen image.  For WebP there are a few Rust implementations which port the C libwep implementation.  Similarly, for AVIF, you can find ports of the C rav1e library.  These are not ideal for writing WASM modules in Rust, and pure Rust implementations are favoured.  For AVIF, the `ravif` library is a potential pure Rust option.  I still need to do some feasibility work here.

## 🙌🏽 Deno Fresh Responsive Images Wrapping Up

We saw some thoughts on working with Deno Fresh responsive images in Deno.  In particular, you saw:
- how you can **generate responsive image markup** using `unpic`
- how you might implement **content negotiation** for delivering the optimal image
- the building blocks for an approach to a self-hosted image API

The <a href="https://github.com/rodneylab/deno/tree/main/demos/deno-fresh-responsive-images">complete code for this project (including Rust source) is in the Rodney Lab GitHub repo</a>.  I do hope the post has either helped you in an existing project or provided some inspiration for a new project.

Get in touch if you have some questions about this content or suggestions for improvements.  You can add a comment below if you prefer.  Also, reach out with ideas for fresh content on Deno or other topics!

## 🙏🏽 Deno Fresh Responsive Images: Feedback

Have you found the post useful? Would you prefer to see posts on another topic instead? Get in touch with ideas for new posts. Also, if you like my writing style, get in touch if I can write some posts for your company site on a consultancy basis. Read on to find ways to get in touch, further below. If you want to support posts similar to this one and can spare a few dollars, euros or pounds, then please <a aria-label="Support Rodney Lab via Buy me a Coffee" href="https://rodneylab.com/giving/">consider supporting me through Buy me a Coffee</a>.


Finally, feel free to share the post on your social media accounts for all your followers who will find it useful. As well as leaving a comment below, you can get in touch via <a aria-label="@ ask Rodney" href="https://twitter.com/messages/compose?recipient_id=1323579817258831875">@askRodney</a> on Twitter, <a href="https://toot.community/@rodney">@rodney@toot.community</a> on Mastodon and also the <a aria-label="Join the Rodney Lab Element Matrix room" href="https://matrix.to/#/%23rodney:matrix.org">#rodney</a> Element Matrix room. Also, see <a aria-label="Get in touch with Rodney Lab" href="https://rodneylab.com/contact/">further ways to get in touch with Rodney Lab</a>. I post regularly on <a aria-label="See posts on Astro" href="https://rodneylab.com/tags/astro/">Astro</a> as well as <a aria-label="See posts on deno" href="https://rodneylab.com/tags/deno/">Deno</a>. Also, <a aria-label="Subscribe to the Rodney Lab newsletter" href="https://newsletter.rodneylab.com/issue/latest-issue">subscribe to the newsletter to keep up-to-date</a> with our latest projects.