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 (
    
the Fresh logo: a sliced lemon dripping with juice

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.