After Brendan Eich’s conception of the language itself, the second most momentous event in JavaScript history (so far) was Ryan Dahl’s humble suggestion that JavaScript could run as an event loop on the server. This notion eventually became Node.js, the runtime that altered the trajectory of Internet-based software development.
Dahl has since launched Deno, a server-side JavaScript framework designed to address Node’s shortcomings. Now, Deno includes a front-end framework called Deno Fresh. Let’s take it for a spin.
What’s different about Deno Fresh
Probably the quickest way to understand Deno Fresh is to think of pairing Next.js with Node or Bun. The result is a front-end JavaScript framework with file-system routing conventions. What makes Fresh different is that it defaults to server-side rendering (SSR) and uses islands, a concept pioneered by Astro.js, to handle front-end interactivity.
Islands bring great performance, and file-system routes keep things simple. We’ll look at both, but first, let’s set up a project to work with.
Create a project with Deno Fresh
The documentation for Deno Fresh (and for Deno generally) is clean and well-organized. Assuming you have Deno installed, you can create a new project using the following command:
$ deno run -A -r https://fresh.deno.dev
A short interactive prompt lets you define the project name (we’ll use the default, fresh-project
), then you can run the project with:
/fresh-project $ deno task start
Now we have a project. Let’s see how Deno Fresh handles file-system routing.
File-system routing in Deno Fresh
Fresh’s routing convention is inspired by Next.js, where files in the /routes
directory are mapped automatically to requests based on the URL. This eliminates the need for a separate artifact that defines the mappings. Looking inside our fresh-project
, we see:
/fresh-project $ ls routes/
_404.tsx _app.tsx api greet index.tsx
These provide your default error (_404.tsx
), global page template (_app.tsx
), and the main route handler, index.tsx
. The index.tsx
page is rendered when a client arrives at this route (http://localhost:8000) without a page name.
The two directories provide two subroutes, accessed at /api
and /greet
. Inside each directory is a file that handles requests:
/routes/api/joke.tsx
import { FreshContext } from "$fresh/server.ts";
// Jokes courtesy of https://punsandoneliners.com/randomness/programmer-jokes/
const JOKES = [ /* array of jokes here */ ];
export const handler = (_req: Request, _ctx: FreshContext): Response => {
const randomIndex = Math.floor(Math.random() * JOKES.length);
const body = JOKES[randomIndex];
return new Response(body);
};
This file is a simple script for sending back the text of a joke. The main point here is that we have a server-side endpoint doing back-end work. In Fresh, this is known as a custom handler. It lets you define pure server-side functionality without a view. The most obvious use for this is in defining back-end API endpoints, RESTful or otherwise.
This is a front-end component that uses a URL param (a “slug”) called name
:
/routes/greet/[name].tsx
import { PageProps } from "$fresh/server.ts";
export default function Greet(props: PageProps) {
return Hello {props.params.name};
}
You can also do partial route matching in the URL path like so: /foo/:bar
. This will only match on /foo/
with a bar
parameter present, and not sub-paths.
Islands in Deno Fresh
You’ve seen some of the main features of the file-system router in Deno Fresh. Now let’s look at how islands are implemented.
Right next to the /routes
directory is an /islands
directory with a single file:
islands/Counter.tsx
import type { Signal } from "@preact/signals";
import { Button } from "../components/Button.tsx";
interface CounterProps {
count: Signal;
}
export default function Counter(props: CounterProps) {
return (
{props.count}
);
}
Fresh knows this file is an island because it lives in the /islands
directory. This means Fresh will render the file on the front end. It’ll ship the minimum amount of front-end JavaScript to handle just this “island” of interactivity. Then, it can be used in a variety of places, even by elements that are fully rendered on the server, where they can be optimized, pre-rendered, and shipped in a compact form. In theory, at least, this setup gives you the best of both worlds. Incorporating the island concept into file routing is a compelling idea.
If we return to the main index.tsx
file, you’ll see how the island is used:
/routes/index.tsx
import { useSignal } from "@preact/signals";
import Counter from "../islands/Counter.tsx";
export default function Home() {
const count = useSignal(3);
return (
Welcome to Fresh
Try updating this message in the
./routes/index.tsx
file, and refresh.
);
}
The main thing to notice here is the inclusion of the Counter
component (import Counter from "../islands/Counter.tsx"
) and its use in the normal JSX markup. This is a simple and direct means of combining server-side rendered code with front-end island functionality.
Reactive state management with signals
You’ll notice these files are using the Signal
type from @preact/signals
. Signals have been gaining mindshare lately as a way to reactively manage state. The Preact homepage has a deep dive on the concept.
Signals are fairly simple to use:
const count = useSignal(3);
This code creates a signal hook, which is initialized to the value of 3. The signal can then be used anywhere, including being passed into a subcomponent for cross-component interaction:
In the Counter
itself, we accept the signal as a prop:
export default function Counter(props: CounterProps)
Through reactive magic, we can modify the signal in the Counter
:
Finally, we can display the value reactively:
{props.count}
Built-in form handling
Fresh also gives you a built-in way to handle forms. This is nice because form handling is a common need, and Fresh helps keep it within the realm of standard browser interactions. The form submission is handled by custom server-side handlers. Fresh lets you respond to these requests with page markup defined by a JSX view. It automatically handles the form data based on whether it’s a GET or a POST request.
TypeScript first
Deno Fresh uses TypeScript as a first-class citizen throughout, including in front-end tsx
files. This is a nice feature for TypeScript users. Interestingly, Node is now taking inspiration from Deno and incorporating TypeScript directly into that platform, as well. In general, TypeScript lets you adopt strong typing where you want it and stick to JavaScript everywhere else.
Build and deploy with Deno Fresh
Fresh is designed to work seamlessly with Deno’s integrated deployment feature, Deno Deploy. You can learn more about using Deno Fresh for deployment here. The idea is similar to Vercel in that it combines a streamlined deployment mechanism with a global edge network.
Fresh doesn’t require a separate build step. All code transformations are done on the fly as needed. Combined with the deployment option, this feature makes for a simpler overall process. You don’t have to bother with messaging a build tool like WebPack or Vite into a production build.
Fresh also has a fast dev mode, which you will notice when making edits.
Conclusion
What I like most about Deno Fresh is how it gathers the best ideas from various sources and brings them together in a cohesive framework. I don’t want to wrestle with composing the bits and pieces of tools I like if I don’t have to. If a comprehensive platform lets me do everything I need for a given project, I can put more thought into design and coding and less into juggling tools.
I’m curious about how Deno Fresh will impact Deno and its place in the server-side JavaScript ecosystem. So far, Deno hasn’t had a killer use case to distinguish it from Node. Deno Fresh might be that thing.