Fresh is a full-stack web framework for Deno that uses the island architecture for optimal performance. It combines server-side rendering with interactive islands. This comprehensive guide covers everything you need to know.
What is Fresh?
Fresh is a web framework that sends zero JavaScript to the client by default, only loading JavaScript for interactive components (islands).
// routes/index.tsx
import { Handlers, PageProps } from "$fresh/server.ts";
export const handler: Handlers = {
GET(_req, ctx) {
return ctx.render({ name: "World" });
},
};
export default function Home({ data }: PageProps) {
return (
<div>
<h1>Hello, {data.name}!</h1>
</div>
);
}
Key Features
- Island Architecture - Send JS only where needed
-
- Server-Side Rendering - Fast initial loads
-
- TypeScript - Built-in TypeScript support
-
- Deno - Uses Deno runtime exclusively
-
- No Build Step - Runs directly
-
- Tailwind CSS - Built-in support
Installation
Creating a New Project
# Create a new Fresh project
deno run -A -r https://fresh.deno.dev my-project
# Navigate to project
cd my-project
# Run development server
deno task start
# Visit http://localhost:8000
Project Structure
my-fresh-app/
โโโ deno.json
โโโ fresh.gen.ts
โโโ main.ts
โโโ dev.ts
โโโ routes/
โ โโโ _app.tsx # App wrapper
โ โโโ _layout.tsx # Shared layout
โ โโโ index.tsx # Home page
โ โโโ about.tsx # About page
โโโ islands/
โ โโโ Counter.tsx # Interactive component
โ โโโ TodoList.tsx # Interactive component
โโโ components/
โ โโโ Button.tsx # Static component
โ โโโ Header.tsx # Static component
โโโ static/
โ โโโ logo.png
โโโ import_map.json
Routes
Basic Route
// routes/index.tsx
import { Handlers, PageProps } from "$fresh/server.ts";
interface Data {
message: string;
}
export const handler: Handlers<Data> = {
GET(_req, ctx) {
return ctx.render({ message: "Hello from Fresh!" });
},
};
export default function Home({ data }: PageProps<Data>) {
return (
<main>
<h1>{data.message}</h1>
</main>
);
}
Route with Path Parameters
// routes/users/[id].tsx
import { Handlers, PageProps } from "$fresh/server.ts";
interface User {
id: string;
name: string;
email: string;
}
export const handler: Handlers<User> = {
async GET(_req, ctx) {
const { id } = ctx.params;
const user = await fetchUser(id);
if (!user) {
return new Response("User not found", { status: 404 });
}
return ctx.render(user);
},
};
export default function UserPage({ data }: PageProps<User>) {
return (
<div>
<h1>{data.name}</h1>
<p>{data.email}</p>
</div>
);
}
async function fetchUser(id: string): Promise<User | null> {
// Fetch user from database
return { id, name: "John", email: "[email protected]" };
}
POST Routes
// routes/api/greet.ts
import { Handlers } from "$fresh/server.ts";
interface FormData {
name: string;
}
export const handler: Handlers = {
async POST(req) {
const formData = await req.formData();
const name = formData.get("name")?.toString() || "Anonymous";
return new Response(JSON.stringify({ greeting: `Hello, ${name}!` }), {
headers: { "Content-Type": "application/json" },
});
},
};
Route Groups
// routes/dashboard/index.tsx
// routes/dashboard/settings.tsx
// These share a common layout (optional)
// routes/dashboard/_layout.tsx
import { HandlerContext } from "$fresh/server.ts";
export default function DashboardLayout(
req: Request,
ctx: HandlerContext,
) {
return (
<div class="dashboard">
<nav>Dashboard Nav</nav>
<ctx.Component />
</div>
);
}
Islands
Islands are interactive components that are hydrated on the client.
Creating an Island
// islands/Counter.tsx
import { useSignal } from "@preact/signals";
export default function Counter() {
const count = useSignal(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => count.value++}>
Increment
</button>
</div>
);
}
Using an Island
// routes/index.tsx
import Counter from "../islands/Counter.tsx";
export default function Home() {
return (
<div>
<h1>Counter Demo</h1>
<Counter />
</div>
);
}
Island with Props
// islands/Clock.tsx
import { useSignal } from "@preact/signals";
interface Props {
initialTime: string;
}
export default function Clock({ initialTime }: Props) {
const time = useSignal(initialTime);
// Update time every second
setInterval(() => {
time.value = new Date().toLocaleTimeString();
}, 1000);
return <p>Time: {time}</p>;
}
// routes/time.tsx
import Clock from "../islands/Clock.tsx";
export default function TimePage() {
const now = new Date().toLocaleTimeString();
return (
<div>
<h1>Current Time</h1>
<Clock initialTime={now} />
</div>
);
}
Multiple Islands
// islands/TodoList.tsx
import { useSignal } from "@preact/signals";
interface Todo {
id: number;
text: string;
done: boolean;
}
export default function TodoList() {
const todos = useSignal<Todo[]>([
{ id: 1, text: "Learn Fresh", done: false }
]);
const input = useSignal("");
const add = () => {
if (!input.value.trim()) return;
todos.value = [
...todos.value,
{ id: Date.now(), text: input.value, done: false }
];
input.value = "";
};
const toggle = (id: number) => {
todos.value = todos.value.map(t =>
t.id === id ? { ...t, done: !t.done } : t
);
};
return (
<div>
<ul>
{todos.value.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.done}
onChange={() => toggle(todo.id)}
/>
{todo.text}
</li>
))}
</ul>
<input
value={input}
onInput={(e) => input.value = e.currentTarget.value}
onKeyDown={(e) => e.key === "Enter" && add()}
/>
<button onClick={add}>Add</button>
</div>
);
}
Components
Static server-rendered components (not hydrated):
// components/Button.tsx
interface Props {
children: preact.ComponentChildren;
onClick?: () => void;
variant?: "primary" | "secondary";
}
export default function Button(
{ children, onClick, variant = "primary" }: Props,
) {
const base = "px-4 py-2 rounded";
const styles = {
primary: "bg-blue-500 text-white",
secondary: "bg-gray-200 text-gray-800",
};
return (
<button
class={`${base} ${styles[variant]}`}
onClick={onClick}
>
{children}
</button>
);
}
Layouts
App Layout
// routes/_app.tsx
import { AppProps } from "$fresh/server.ts";
export default function App({ Component }: AppProps) {
return (
<html>
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Fresh App</title>
</head>
<body>
<Component />
</body>
</html>
);
}
Page Layout
// routes/_layout.tsx
import { Handlers, PageProps } from "$fresh/server.ts";
export default function Layout(req: Request, ctx: HandlerContext) {
return (
<div>
<header>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
</header>
<main>
<ctx.Component />
</main>
<footer>
<p>My Fresh App</p>
</footer>
</div>
);
}
Data Fetching
Server-Side Data
// routes/posts/index.tsx
import { Handlers, PageProps } from "$fresh/server.ts";
interface Post {
id: string;
title: string;
body: string;
}
export const handler: Handlers<Post[]> = {
async GET(_req, ctx) {
const posts = await fetchPosts();
return ctx.render(posts);
},
};
export default function PostsIndex({ data }: PageProps<Post[]>) {
return (
<div>
<h1>Posts</h1>
<ul>
{data.map((post) => (
<li>
<a href={`/posts/${post.id}`}>{post.title}</a>
</li>
))}
</ul>
</div>
);
}
async function fetchPosts(): Promise<Post[]> {
// Fetch from database or API
return [
{ id: "1", title: "Post 1", body: "Content..." },
{ id: "2", title: "Post 2", body: "Content..." },
];
}
API Routes
// routes/api/posts/[id].ts
import { Handlers } from "$fresh/server.ts";
export const handler: Handlers = {
async GET(_req, ctx) {
const { id } = ctx.params;
const post = await getPost(id);
if (!post) {
return new Response("Not found", { status: 404 });
}
return new Response(JSON.stringify(post), {
headers: { "Content-Type": "application/json" },
});
},
async DELETE(_req, ctx) {
const { id } = ctx.params;
await deletePost(id);
return new Response(null, { status: 204 });
},
};
async function getPost(id: string) {
return { id, title: "Post", body: "Content" };
}
async function deletePost(id: string) {
// Delete from database
}
Forms and Actions
Form Handling
// routes/contact.tsx
import { Handlers, PageProps } from "$fresh/server.ts";
interface FormData {
name: string;
message: string;
}
export const handler: Handlers<FormData | null> = {
GET(_req, ctx) {
return ctx.render(null);
},
async POST(req, ctx) {
const formData = await req.formData();
const data: FormData = {
name: formData.get("name")?.toString() || "",
message: formData.get("message")?.toString() || "",
};
// Process form
await sendEmail(data);
return ctx.render(data);
},
};
export default function Contact({ data }: PageProps<FormData | null>) {
return (
<div>
{data ? (
<p>Thank you, {data.name}!</p>
) : (
<form method="post">
<input type="text" name="name" placeholder="Name" required />
<textarea name="message" placeholder="Message" required />
<button type="submit">Send</button>
</form>
)}
</div>
);
}
async function sendEmail(data: FormData) {
// Send email...
}
State Management
Using Signals
// islands/Counter.tsx
import { useSignal } from "@preact/signals";
export default function Counter() {
const count = useSignal(0);
const double = useSignal(0);
// Computed value
double.value = count.value * 2;
return (
<div>
<p>Count: {count}</p>
<p>Double: {double}</p>
<button onClick={() => count.value++}>Increment</button>
</div>
);
}
Context
// islands/AppContext.tsx
import { createContext } from "@preact/signals";
import { useContext } from "preact/hooks";
export const ThemeContext = createContext("light");
export function useTheme() {
return useContext(ThemeContext);
}
// Usage
const theme = useTheme();
Styling
Tailwind CSS
Fresh has built-in Tailwind support:
// Any component
export default function Styled() {
return (
<div class="p-4 bg-blue-500 text-white rounded-lg">
<h1 class="text-2xl font-bold">Hello</h1>
</div>
);
}
Scoped Styles
// components/Button.tsx
export default function Button({ children }) {
return (
<style>{`
.btn {
padding: 8px 16px;
border-radius: 4px;
background: blue;
color: white;
}
`}</style>
<button class="btn">{children}</button>
);
}
Middleware
// routes/_middleware.ts
import { FreshContext } from "$fresh/server.ts";
export function handler(req: Request, ctx: FreshContext) {
const url = new URL(req.url);
// Add custom header
const response = ctx.next();
response.headers.set("X-Custom", "value");
return response;
}
External Resources
Conclusion
Fresh provides an excellent developer experience for building modern web applications with Deno. Key points:
- Island architecture for minimal JavaScript
- Server-side rendering for fast initial loads
- Built-in TypeScript support
- Tailwind CSS integration
- Zero config required
For Deno-based projects, Fresh offers a modern, performant approach to full-stack development.
Comments