examples/tutorials/fresh.md
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.
:::info 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!
:::
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:
my-fresh-appNavigate into your new project directory:
cd my-fresh-app
Start the development server:
deno task dev
Open your browser to http://localhost:5173 to see your new Fresh app running!
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
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.
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.
<main>
<h1>🦕 Welcome to the Dinosaur Encyclopedia</h1>
<p>Click on a dinosaur below to learn more.</p>
<div class="dinosaur-list">
</div>
</main>;
We'll make a new component which will be used to display each dinosaur in the list.
Create a new file at components/LinkButton.tsx and add the following code:
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.
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>
</>
);
});
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.
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>
);
}
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:
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:
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:
<FavoriteButton />;
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:
<link rel="stylesheet" href="https://demo-styles.deno.deno.net/styles.css" />;
Make sure your development server is running with:
deno task dev
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.
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
This will build out the optimized files to a directory called _fresh.
To run the built app, you can use the start task, which will automatically
pick up the optimized assets in the _fresh directory:
deno task start
Open your browser to http://localhost:8000 to see the production version of
your app.
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 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
Once your app is on GitHub, you can deploy it to Deno Deploy.
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:
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.