Deno Fresh Getting Started: Islands, APIs & Testing
Rodney Lab
Posted on February 3, 2023
π¬ 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>
);
}
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.
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>
);
};
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:
- import the
asset
named import function from$fresh/runtime.ts
, - 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>
);
};
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;
}
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 ...*/
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>
);
};
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
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"
}
}
Then to rattle off tests, in the Terminal type:
deno task test
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>
`,
);
});
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!");
};
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",
});
For convenience there are also Response.redirect
and Response.json
, providing a spot of syntactic sugar:
return Response.redirect(`https://example.com/home`, 301);
return Response.json({ message, data });
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 });
},
};
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;
}
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.
Posted on February 3, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.