Skip to main content
On this page

Build a Fresh App

Fresh is a full-stack web framework for Deno that emphasizes server-side rendering with islands of interactivity. It sends no JavaScript to the client by default, making it incredibly fast and efficient. Fresh uses a file-based routing system and leverages Deno's modern runtime capabilities.

In this tutorial, we'll build a simple dinosaur catalog app that demonstrates Fresh's key features. The app will display a list of dinosaurs, allow you to view individual dinosaur details, and include interactive components using Fresh's islands architecture.

You can see the finished app repo on GitHub and a demo of the app on Deno Deploy.

Deploy your own

Want to skip the tutorial and deploy the finished app right now? Click the button below to instantly deploy your own copy of the complete Fresh dinosaur app to Deno Deploy. You'll get a live, working application that you can customize and modify as you learn!

Deploy on Deno

Create a Fresh project Jump to heading

Fresh provides a convenient scaffolding tool to create a new project. In your terminal, run the following command:

deno run -Ar jsr:@fresh/init

This command will:

  • Download the latest Fresh scaffolding script
  • Create a new directory called my-fresh-app
  • Set up a basic Fresh project structure
  • Install all necessary dependencies

Navigate into your new project directory:

cd my-fresh-app

Start the development server:

deno task start

Open your browser to http://localhost:5173 to see your new Fresh app running!

Understanding the project structure Jump to heading

The project contains the following key directories and files:

my-fresh-app/
├── assets/           # Static assets (images, CSS, etc.)
├── components/        # Reusable UI components
├── islands/           # Interactive components (islands)
├── routes/            # File-based routing
│  └── api/             # API routes
├── static/            # Static assets (images, CSS, etc.)
├── main.ts            # Entry point of the application
├── deno.json          # Deno configuration file
└── README.md          # Project documentation

Adding dinosaur data Jump to heading

To add dinosaur data to our app, we'll create a simple data file which contains some information about dinosaurs in json. In a real application, this data might come from a database or an external API, but for simplicity, we'll use a static file.

In the routes/api directory, create a new file called data.json and copy the content from here.

Displaying the dinosaur list Jump to heading

The homepage will display a list of dinosaurs that the user can click on to view more details. Lets update the routes/index.tsx file to fetch and display the dinosaur data.

First update the <title> in the head of the file to read "Dinosaur Encyclopedia". Then we'll add some basic HTML to introduce the app.

index.tsx
<main>
  <h1>🦕 Welcome to the Dinosaur Encyclopedia</h1>
  <p>Click on a dinosaur below to learn more.</p>
  <div class="dinosaur-list">
    {/* Dinosaur list will go here */}
  </div>
</main>;

We'll make a new component which will be used to display each dinosaur in the list.

Creating a component Jump to heading

Create a new file at components/LinkButton.tsx and add the following code:

LinkButton.tsx
import type { ComponentChildren } from "preact";

export interface LinkButtonProps {
  href?: string;
  class?: string;
  children?: ComponentChildren;
}

export function LinkButton(props: LinkButtonProps) {
  return (
    <a
      {...props}
      class={"btn " +
        (props.class ?? "")}
    />
  );
}

This component renders a styled link that looks like a button. It accepts href, class, and children props.

finally, update the routes/index.tsx file to import and use the new LinkButton component to display each dinosaur in the list.

index.tsx
import { Head } from "fresh/runtime";
import { define } from "../utils.ts";
import data from "./api/data.json" with { type: "json" };
import { LinkButton } from "../components/LinkButton.tsx";

export default define.page(function Home() {
  return (
    <>
      <Head>
        <title>Dinosaur Encyclopedia</title>
      </Head>
      <main>
        <h1>🦕 Welcome to the Dinosaur Encyclopedia</h1>
        <p>Click on a dinosaur below to learn more.</p>
        <div class="dinosaur-list">
          {data.map((dinosaur: { name: string; description: string }) => (
            <LinkButton
              href={`/dinosaurs/${dinosaur.name.toLowerCase()}`}
              class="btn-primary"
            >
              {dinosaur.name}
            </LinkButton>
          ))}
        </div>
      </main>
    </>
  );
});

Creating dynamic routes Jump to heading

Fresh allows us to create dynamic routes using file-based routing. We'll create a new route to display individual dinosaur details.

Create a new file at routes/dinosaurs/[name].tsx. In this file, we'll fetch the dinosaur data based on the name parameter and display it.

[dinosaur].tsx
import { PageProps } from "$fresh/server.ts";
import data from "../api/data.json" with { type: "json" };
import { LinkButton } from "../../components/LinkButton.tsx";

export default function DinosaurPage(props: PageProps) {
  const name = props.params.dinosaur;
  const dinosaur = data.find((d: { name: string }) =>
    d.name.toLowerCase() === name.toLowerCase()
  );

  if (!dinosaur) {
    return (
      <main>
        <h1>Dinosaur not found</h1>
      </main>
    );
  }

  return (
    <main>
      <h1>{dinosaur.name}</h1>
      <p>{dinosaur.description}</p>
      <LinkButton href="/" class="btn-secondary">← Back to list</LinkButton>
    </main>
  );
}

Adding interactivity with islands Jump to heading

Fresh's islands architecture allows us to add interactivity to specific components without sending unnecessary JavaScript to the client. Let's create a simple interactive component that allows users to "favorite" a dinosaur.

Create a new file at islands/FavoriteButton.tsx and add the following code:

FavoriteButton.tsx
import { useState } from "preact/hooks";

export default function FavoriteButton() {
  const [favorited, setFavorited] = useState(false);

  return (
    <button
      type="button"
      className={`btn fav ${favorited ? "btn-favorited" : "btn-primary"}`}
      onClick={() => setFavorited((f) => !f)}
    >
      {favorited ? "★ Favorited!" : "☆ Add to Favorites"}
    </button>
  );
}

This is just a simple button that toggles its state when clicked. You could update it to store the favorite state in a database or local storage for a more complete feature.

Now we need to import and use this FavoriteButton island in our dinosaur detail page. Add the import at the top of routes/dinosaurs/[dinosaur].tsx:

[dinosaur].tsx
import FavoriteButton from "../../islands/FavoriteButton.tsx";

and then include the <FavoriteButton /> component in the JSX where you want it to appear, for example, before the back button:

[dinosaur].tsx
<FavoriteButton />;

Styling the app Jump to heading

We've created some basic styles to add to your app, but of course you can add your own css in the assets/styles.css file. Add a link to our provided stylesheet in the <head> of routes/_app.tsx:

_app.tsx
<link rel="stylesheet" href="https://demo-styles.deno.deno.net/styles.css" />;

Running the app Jump to heading

Make sure your development server is running with:

deno task start

Open your browser to http://localhost:5173 to see your dinosaur catalog app in action! You should be able to view the list of dinosaurs, click on one to see its details, and use the "Favorite" button to toggle its favorite status.

Build and deploy Jump to heading

The default Fresh app comes with a build task that builds the app with Vite. You can run the following command to build the app for production mode:

deno run build

You can deploy this app to your favorite cloud provider. We recommend using Deno Deploy for a simple and easy deployment experience. You can deploy your app directly from GitHub, simply create a GitHub repository and push your code there, then connect it to Deno Deploy.

Create a GitHub repository Jump to heading

Create a new GitHub repository, then initialize and push your app to GitHub:

git init -b main
git remote add origin https://github.com/<your_github_username>/<your_repo_name>.git
git add .
git commit -am 'my fresh app'
git push -u origin main

Deploy to Deno Deploy Jump to heading

Once your app is on GitHub, you can deploy it to Deno DeployEA.

For a walkthrough of deploying your app, check out the Deno Deploy tutorial.

🦕 Now you have a starter Fresh app! Here are some ideas to extend your dinosaur catalog:

  • Add a database (try Deno KV or connect to PostgreSQL)
  • Implement user authentication with
  • Add more interactive features like favorites or ratings
  • Connect to external APIs for more dinosaur data

Fresh's architecture makes it easy to build fast, scalable web applications while maintaining a great developer experience. The combination of server-side rendering by default with optional client-side interactivity gives you the best of both worlds.

Did you find what you needed?

Privacy policy