Deno Fresh Getting Started: Islands, APIs & Testing

askrodney

Rodney Lab

Posted on February 3, 2023

Deno Fresh Getting Started: Islands, APIs & Testing

🎬 Getting Started with Deno Fresh: Part II

In this second post on Deno Fresh getting started, we see how Fresh islands of interactivity work. As well as that we look how you can use Deno’s in-built testing in your Fresh app.

In the first part of the series we saw how you can add web pages to your Fresh app using its file-based routing system. We extend on what we saw there, in this follow-up, to see how you can also add API routes as well as resource routes (serving an XML RSS feed, for example).

We started the first post looking at why you might consider Deno Fresh and saw how you can set up Deno on your system and spin up your first Fresh app. If you are new here, please do skim through the previous post if something here does not click.

We will start with a quick introduction to partial hydration and the Islands architecture. Then we will see how Deno implements it’s opt-in Island components philosophy. Then we move on to look at testing and API routes.

πŸ€” Why Deno Fresh for Content Sites?

Deno Fresh is a fantastic choice for content sites. Here we are talking about blog sites, documentation sites and such like. In the near-recent past statically generated sites were the most popular choice for content sites, with something like GatsbyJS a common choice. The reasoning behind choosing static generators over server-side rendered (SSR) ones were principally speed and security. There was a Developer Experience trade-off though, which lay in the fact that sites took long to build (as the static content was generated). However, the user got the benefit when the static content could be served quickly from a global CDNContent Delivery Network: global system of servers distributed close to users.

Deno Fresh is fast despite being a server-side rendered generator. Designed for the modern, serverless world, using Preact it generates pages on the server adding JavaScript only where needed. That last little trick β€” shipping zero JavaScript by default β€” lets Deno apps load faster on the user device. On top, because fewer bytes need to be shipped to the user’s device, the device receives the page data quicker. This pattern, especially with the performance benefits of using Preact over React for rendering, is what makes Fresh a good choice for content sites. Typically the parts of content site page which are JavaScript-heavy are distinct and isolated islands of interactivity. This is what we look at next.

🧡 10 More Deno Fresh Getting Started Tips

Following on from the previous Getting Started with Deno Fresh post here are ten more Deno Fresh getting started tips.

1. 🏝️ Islands

Deno Fresh ships zero JavaScript by default but makes islands available for when you need interactivity. A great example of an Island of Interactivity on a blog post page might be the social sharing buttons. Most of the page will be text (no JavaScript there). There might be a bit of JavaScript to control opening a hamburger menu or for a newsletter sign-up form.

Those instances will typically be isolated to small parts of the page and in fact, by using the platform, we can remove JavaScript completely from the sign-up form. The Deno Fresh philosophy is only to ship JavaScript for those parts of the page which need it, rather than for the whole page. This means we ship less hydration code β€” the code which makes sure state is consistent for interactive parts of the page.

For this to work, we put our component which have state or interactivity in the islands directory of the project, rather than the components directory. So returning to the social share buttons example, we can add that code to a ShareButtons.tsx file, which looks just like any other React component:

export default function ShareButton({
  siteUrl,
  title,
}: ShareButtonProps) {
  const [webShareAPISupported, setWebShareAPISupported] = useState<boolean>(
    true,
  );

  useEffect(() => {
    if (typeof navigator.share === "undefined") setWebShareAPISupported(false);
  }, [webShareAPISupported]);

  const handleClick = () => {
    if (webShareAPISupported) {
      try {
        navigator.share({
          title,
          text: `Rodney Lab Newsletter ${title}`,
          url,
        });
      } catch (error: unknown) {
        setWebShareAPISupported(false);
      }
    }
  };

  return (
    <div class="share">
      {webShareAPISupported
        ? (
          <button onClick={handleClick}>
            <span class="screen-reader-text">Share</span>{" "}
            <ShareIcon width={24} />
          </button>
        )
        : (
          <div class="share-fallback">
            SHARE:
            <div class="share-buttons">
              <TelegramShareButton />
              <TwitterShareButton />
            </div>
          </div>
        )}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Here, we are using the WebShare API with graceful degradation for devices which do not yet support it. To achieve that we:

  • have a useState hook, assuming initially that the device supports the WebShare API,
  • add a useEffect hook to feature detect the WebShare API and update are support assumption is needed,
  • show the WebShare icon when the API is supported but show manual Telegram and Twitter share button otherwise.

All three of those points need JavaScript to work with to track state, to add interactivity or both. This is definitely an island and to let Deno Fresh know it has to manage interactivity for us, we have to put it in the Islands directory.

Deno Fresh Stack: two adjacent screen captures show the same site, one with a Web Share icon, the other with separate Twitter, Whats App and Telegram icons.

2. πŸš‰ Use the Platform

Deno Fresh leans heavily on Web APIs. Using these in your project you can work with Deno, limiting the use of islands and keeping your app lightning fast. For example returning to a blog site, we can add a newsletter sign-up form without needing to ship JavaScript to the browser:

export const Subscribe: FunctionComponent<SubscribeProps> = function Subscribe({
  pathname,
}) {
return (
  <form
    action={pathname}
    method="post"
  >
    <h2>Subscribe to the newsletter</h2>
    <label for="email" class="screen-reader-text">
      Email
    </label>
      <input
        id="email"
        type="text"
        name="email"
        required
      />
      <button type="submit">Subscribe</button>
  </form>
  );
};
Enter fullscreen mode Exit fullscreen mode

Here the opening form tag includes a method attribute set to POST as well as an action. The action can be the pathname for the current page, fed in to this component as a prop.

When the form is submitted, the device now sends a POST request to the same route. We just need to add a POST handler in the Fresh code for this page (much like we had in the Tweet Queue Deno Fresh route file example).

3. πŸ₯Š Static Assets & Cache Busting

You will have files like favicons and perhaps web manifests for PWAsProgressive Web Apps which do not need any processing. You can put them in your project’s static folder. Deno will serve them for you from there. As an example, static/favicon.ico will be served on your built site from https://example.com/favicon.ico.

That’s not all Deno does for you though! There is a handy cache busting feature, even for these static assets! What it does is add a hash to the filename (under the hood). How is this helpful? If you change a favicon (lets say you go for a different colour) and you keep the same file name as before. Deno will recompute the file hash, this will be different to the previous one.

This means browsers and CDNs will know to download the new coloured favicon instead of serving the cached one. To make it work:

  1. import the asset named import function from $fresh/runtime.ts,
  2. when you include the favicon, image, CSS or other asset in a link tag, wrap it in a call to the asset function:
import { asset } from "$fresh/runtime.ts";

export const Layout: FunctionComponent<LayoutProps> =
  function Layout() {
    return (
      <Fragment>
        <Head>
          <link rel="icon" href={asset("/favicon.ico")} sizes="any" />
          <link rel="icon" href={asset("/icon.svg")} type="image/svg+xml" />
          <link rel="apple-touch-icon" href={asset("/apple-touch-icon.png")} />
          <link rel="manifest" href={asset("/manifest.webmanifest")} />
        </Head>
        {children}
      </Fragment>
    );
  };
Enter fullscreen mode Exit fullscreen mode

4. πŸ’„ Styling

You can choose Tailwind for styling in the interactive prompts when you initialise your project. If you prefer vanilla CSS then, of course, Fresh handles that too! You can even self-host fonts! Add the CSS in the static folder (like we mentioned for favicons above). Then just remember to include it in the Head for your page or layout component markup (again just like for favicons):

@font-face {
  font-family: Lato;
  font-style: normal;
  font-weight: 400;
  src: local(""), url("/fonts/lato-v23-latin-regular.woff2") format("woff2"),
    url("/fonts/lato-v23-latin-regular.woff") format("woff");
  font-display: swap;
}
Enter fullscreen mode Exit fullscreen mode

If you do want to self-host the fonts, there is a handy Web Fonts helper which generates the CSS and lets you download the .woff2 files you’ll need.

*,
:after,
:before {
  box-sizing: border-box;
}

* {
  margin: 0;
}

/* TRUNCATED ...*/
Enter fullscreen mode Exit fullscreen mode
import { asset } from "$fresh/runtime.ts";

export const Layout: FunctionComponent<LayoutProps> =
  function Layout() {
    return (
      <Fragment>
        <Head>
          <link rel="icon" href={asset("/favicon.ico")} sizes="any" />
          <link rel="icon" href={asset("/icon.svg")} type="image/svg+xml" />
          <link rel="apple-touch-icon" href={asset("/apple-touch-icon.png")} />
          <link rel="manifest" href={asset("/manifest.webmanifest")} />
          <link rel="stylesheet" href={asset("/styles/global.css")} />
          <link rel="stylesheet" href={asset("/styles/fonts.css")} />
        </Head>
        {children}
      </Fragment>
    );
  };
Enter fullscreen mode Exit fullscreen mode

5. πŸͺš Tooling

We mentioned earlier that there is no need to spend time configuring ESLint or Prettier in each Deno Fresh project you start. Deno comes with its own linter and formatter all with sensible defaults. To run these from the command line, just use:

deno fmt

deno lint
Enter fullscreen mode Exit fullscreen mode

To have VSCode format on save see the VSCode config in the quick tips in the previous Getting Started with Deno Fresh post. If you are a Vim person, try the denols LSP plugin for Neovim.

πŸ‘€ TypeScript

Nothing to see here. TypeScript support come out with Deno out-of-the-box: no need to add a tsconfig.json or typescript-eslint config. Just start coding in TypeScript.

6. β˜‘οΈ Testing

Just like linting and formatting, Deno has testing built in β€” there is 0 test runner config. That said, you might want to set up a test script, just to fire off tests quicker. Update your deno.json file to do this:

{
  "tasks": {
    "start": "deno run -A --watch=static/,routes/ dev.ts",
    "test": "deno test -A",
  },
  "importMap": "./import_map.json",
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "preact"
  }
}
Enter fullscreen mode Exit fullscreen mode

Then to rattle off tests, in the Terminal type:

deno task test
Enter fullscreen mode Exit fullscreen mode

If you have already used Jest or Vite, then there are no surprises when it comes to writing the tests themselves. Import assert and friends from std/testing/asserts and you are already at the races!

import { assert, assertEquals } from "$std/testing/asserts.ts";
import { markdown_to_html } from "@/lib/rs_lib.generated.js";

Deno.test("it parses markdown to html", async () => {
  // arrange
  const markdown = `
## πŸ‘‹πŸ½ Hello You

* alpha
* beta
`;

  // act
  const { errors, html, headings, statistics } = await markdown_to_html(
    markdown,
  );

  // assert
  assert(typeof markdown_to_html === "function");
  assertEquals(
    html,
    `<h2 id="wave-skin-tone-4-hello-you">πŸ‘‹πŸ½ Hello You <a href="#wave-skin-tone-4-hello-you" class="heading-anchor">#</a></h2>
<ul>
<li>alpha</li>
<li>beta</li>
</ul>
`,
  );
});
Enter fullscreen mode Exit fullscreen mode

7. ☎️ API Routes

You might use APIApplication Programming Interface routes to handle back end operations. This is another advantage of using Deno Fresh over a static site generator: API route handling is built in. You effectively get Serverless functions woven into your project.

The actual API route file is essentially just a regular route file handler. You can name the file with a .ts extension in the API case though. Here is an example where we send SMS message via the Twilio API from a Deno Fresh API route:

import { HandlerContext } from "$fresh/server.ts";
import { encode as base64Encode } from "$std/encoding/base64.ts";

const TWILIO_SID = Deno.env.get("TWILIO_SID");
const TWILIO_AUTH_TOKEN = Deno.env.get("TWILIO_AUTH_TOKEN");

export const handler = async (
  _request: Request,
  _ctx: HandlerContext,
): Promise<Response> => {
  if (TWILIO_SID === undefined) {
  throw new Error("env `TWILIO_SID` must be set");
  }
  if (TWILIO_AUTH_TOKEN === undefined) {
    throw new Error("env `TWILIO_AUTH_TOKEN` must be set");
  }

  const authorisationToken = base64Encode(`${TWILIO_SID}:${TWILIO_AUTH_TOKEN}`);
  const body = new URLSearchParams({
    Body: "Hello from Twilio",
    From: "+4412345",
    To: "+4412345",
  });
  const response = await fetch(
    `https://api.twilio.com/2010-04-01/Accounts/${TWILIO_SID}/Messages.json`,
    {
      method: "POST",
      headers: {
        Authorization: `Basic ${authorisationToken}`,
        "Content-Type": "application/x-www-form-urlencoded",
      },
      body,
    },
  );
  const data = await response.json();
  console.log({ data });
  return new Response("Thanks!");
};
Enter fullscreen mode Exit fullscreen mode

Although we put the file in an api subdirectory this is by choice and not necessary.

Notice the Deno way of Base64 encoding BasicAuth parameters. We import the encoder in line 2, then use it in line 18 to generate Base64 string which we need to send in the Basic authorisation header in line 29.

The rest just uses the same JavaScript APIs you already learnt from MDNMozilla Developer Network: popular documentation resource for Web Developers and use in Remix, Astro or SvelteKit. Notice we return with a Response object. You can just as easily return a redirect or server error.

return new Response("Auth failed", {
  status: 502,
  statusText: "Bad Gateway",
});
Enter fullscreen mode Exit fullscreen mode

For convenience there are also Response.redirect and Response.json, providing a spot of syntactic sugar:

return Response.redirect(`https://example.com/home`, 301);
Enter fullscreen mode Exit fullscreen mode
return Response.json({ message, data });
Enter fullscreen mode Exit fullscreen mode

That last one adds extra convenience, saving you having to construct the customary headers manually.

8. πŸ’° Resource Routes

You might use resource routes to serve PDF files, JSON data or even an RSSRDF Site Summary: standard for web feeds allowing subscribers to receive news of updated content feed like this example shows:

import { Handlers } from "$fresh/server.ts";
import { getDomainUrl } from "@/utils/network.ts";
import { lastIssueUpdateDate, loadLatestIssue } from "@/utils/issue.ts";

export const handler: Handlers = {
  async GET(request, _context) {
    const domainUrl = getDomainUrl(request);
    const { publishAt: lastIssuePublishDate } = (await loadLatestIssue()) ?? {};

    const xmlString = `
        <?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet type="text/xsl" href="${domainUrl}/main-sitemap.xsl"?>
        <sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
            <sitemap>
                <loc>${domainUrl}/issue-sitemap.xml</loc>
                <lastmod>${(await lastIssueUpdateDate()).toISOString()}</lastmod>
            </sitemap>
            <sitemap>
                <loc>${domainUrl}/page-sitemap.xml</loc>
                <lastmod>${lastIssuePublishDate?.toISOString()}</lastmod>
            </sitemap>
        </sitemapindex>`.trim();

    const headers = new Headers({
      "Cache-Control": `public, max-age=0, must-revalidate`,
      "Content-Type": "application/xml",
      "x-robots-tag": "noindex, follow",
    });

    return new Response(xmlString, { headers });
  },
};
Enter fullscreen mode Exit fullscreen mode

9. πŸ§‘πŸ½β€πŸ”§ Middleware

Middleware or Edge Functions in Deno let you intercept incoming requests and run code snippets on them before proceeding. Again these are build into Deno Fresh. As well as intercepting the incoming request, you can effectively alter the response. See the video on Deno Fresh Middleware for more explanation on this.

Add a _middleware.ts file to any folder containing route files you want it to apply to:

import { MiddlewareHandlerContext } from "$fresh/server.ts";

export async function handler(
  _request: Request,
  context: MiddlewareHandlerContext,
) {
  const response = await context.next();
  const { ok } = response;

  if (ok) {
    response.headers.set(
      "Strict-Transport-Security",
      "max-age=31536000; includeSubDomains; preload",
    );
    response.headers.set("X-Frame-Options", "DENY");
    response.headers.set("X-Content-Type-Options", "nosniff");
    response.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
  }

  return response;
}
Enter fullscreen mode Exit fullscreen mode

10. πŸ¦€ WASM

WASM lets you write code in C, C++ or Rust but compile to to a module which can run in a JavaScript environment. This lets you leverage efficiency or existing libraries in those ecosystems. WASM is a first-class citizen in Deno and the wasmbuild module will build out a skeleton Rust WASM project and also compile from within your existing Deno project.

Although the process is not complicated, we will not go into it here as there is an example with fully working code on using Rust WASM with Deno Fresh.

πŸ™ŒπŸ½ Deno Fresh Getting Started: Wrapping Up

We have seen a lot in the last two Deno Fresh getting started articles. I have tried to integrate my own learnings and take you beyond what is in the Deno Fresh docs. I do hope this has been useful for you especially covering helpful details for anyone new to Deno. In particular, we have seen:

  • how Deno Fresh islands work,
  • examples of using the platform with Deno Fresh,
  • an introduction to more advanced Deno Fresh features like WASM, API and resource routes.

Get in touch if you would like to see more content on Deno and Fresh. Let me know if indeed you have found the content useful or even if you have some possible improvements.

πŸ™πŸ½ Deno Fresh Getting Started: 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 consider supporting me through Buy me a Coffee.

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 @askRodney on Twitter, @rodney@toot.community on Mastodon and also the #rodney Element Matrix room. Also, see further ways to get in touch with Rodney Lab. I post regularly on Astro as well as Deno. Also subscribe to the newsletter to keep up-to-date with our latest projects.

πŸ’– πŸ’ͺ πŸ™… 🚩
askrodney
Rodney Lab

Posted on February 3, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related