Deno's new front end offers a full-stack implementation of everything TypeScript developers love about Deno, but it doesn't stop there. Get started with file-system routes, islands, and signals in Deno Fresh. Credit: Spalnic / Shutterstock 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 <div>Hello {props.params.name}</div>; } 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<number>; } export default function Counter(props: CounterProps) { return ( <div class="flex gap-8 py-6"> <Button onClick={() => props.count.value -= 1}>-1</Button> <p class="text-3xl tabular-nums">{props.count}</p> <Button onClick={() => props.count.value += 1}>+1</Button> </div> ); } 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 ( <div class="px-4 py-8 mx-auto bg-[#86efac]"> <div class="max-w-screen-md mx-auto flex flex-col items-center justify-center"> <img class="my-6" src="/logo.svg" width="128" height="128" alt="the Fresh logo: a sliced lemon dripping with juice" /> <h1 class="text-4xl font-bold">Welcome to Fresh</h1> <p class="my-4"> Try updating this message in the <code class="mx-2">./routes/index.tsx</code> file, and refresh. </p> <Counter count={count} /> </div> </div> ); } 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: <Counter count={count} /> 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: <Button onClick={() => props.count.value += 1}>+1</Button> Finally, we can display the value reactively: <p class="text-3xl tabular-nums">{props.count}</p> 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. Related content feature 14 great preprocessors for developers who love to code Sometimes it seems like the rules of programming are designed to make coding a chore. Here are 14 ways preprocessors can help make software development fun again. By Peter Wayner Nov 18, 2024 10 mins Development Tools Software Development feature Designing the APIs that accidentally power businesses Well-designed APIs, even those often-neglected internal APIs, make developers more productive and businesses more agile. By Jean Yang Nov 18, 2024 6 mins APIs Software Development news Spin 3.0 supports polyglot development using Wasm components Fermyon’s open source framework for building server-side WebAssembly apps allows developers to compose apps from components created with different languages. By Paul Krill Nov 18, 2024 2 mins Microservices Serverless Computing Development Libraries and Frameworks news Go language evolving for future hardware, AI workloads The Go team is working to adapt Go to large multicore systems, the latest hardware instructions, and the needs of developers of large-scale AI systems. By Paul Krill Nov 15, 2024 3 mins Google Go Generative AI Programming Languages Resources Videos