Fresh Framework
Fresh full-stack web framework for Deno with islands architecture and zero client JS by default
You are an expert in the Fresh framework for building full-stack web applications on Deno with islands-based partial hydration. ## Key Points - **Default to server components** — only promote to an island when the component needs browser interactivity (click handlers, client state, animations). - **Keep islands small and leaf-level** — wrapping a large subtree in an island defeats the purpose. Push interactivity to the leaves. - **Use Preact Signals** instead of `useState` in islands for fine-grained reactivity without re-renders. - **Colocate data fetching in handlers** — do not fetch data inside components. Handlers run server-side and can access KV, databases, and secrets safely. - **Use progressive enhancement** — forms work without JavaScript via standard `<form method="POST">`. Add islands only for richer UX. - **Deploy to Deno Deploy** for zero-config edge hosting; Fresh is designed for it. - **Putting components in `islands/` that don't need hydration** — this ships unnecessary JavaScript. Only truly interactive components belong there. - **Passing non-serializable props to islands** — island props are serialized to JSON. Functions, classes, and circular references will break. - **Using Node/npm packages that access `window` at import time** — server-side rendering will fail. Guard browser-only code with `IS_BROWSER` from `$fresh/runtime.ts`. - **Forgetting that `_layout.tsx` wraps all sibling and nested routes** — unexpected layout nesting happens when layout files exist at multiple directory levels. - **Large static assets in `static/`** — they are bundled into the deployment. Use an external CDN for large media files.
skilldb get deno-bun-skills/Fresh FrameworkFull skill: 261 linesFresh Framework — Modern JS Runtimes
You are an expert in the Fresh framework for building full-stack web applications on Deno with islands-based partial hydration.
Overview
Fresh is a full-stack web framework for Deno that ships zero JavaScript to the client by default. It uses an "islands architecture" where only explicitly interactive components are hydrated in the browser. Pages are rendered server-side using Preact (JSX), routing is file-system based, and deployments target Deno Deploy out of the box.
Core Concepts
Islands Architecture
The page shell and all static content render on the server as plain HTML. Only components placed in the islands/ directory get a client-side JavaScript bundle and hydrate in the browser. Everything else stays server-only.
File-System Routing
project/
routes/
index.tsx -> /
about.tsx -> /about
blog/
index.tsx -> /blog
[slug].tsx -> /blog/:slug
api/
users.ts -> /api/users
_layout.tsx -> wrapping layout
_middleware.ts -> runs before route handlers
_404.tsx -> custom 404 page
islands/
Counter.tsx -> hydrated client component
components/
Header.tsx -> server-only component (no JS shipped)
static/
styles.css -> served as-is
fresh.config.ts
deno.json
Handlers and Page Components
A route file exports a handler for data fetching and a default component for rendering:
import { Handlers, PageProps } from "$fresh/server.ts";
interface Post {
title: string;
body: string;
}
export const handler: Handlers<Post> = {
async GET(_req, ctx) {
const slug = ctx.params.slug;
const resp = await fetch(`https://api.example.com/posts/${slug}`);
if (!resp.ok) return ctx.renderNotFound();
const post: Post = await resp.json();
return ctx.render(post);
},
};
export default function PostPage({ data }: PageProps<Post>) {
return (
<article>
<h1>{data.title}</h1>
<p>{data.body}</p>
</article>
);
}
Implementation Patterns
Creating an Island
// islands/Counter.tsx
import { useSignal } from "@preact/signals";
export default function Counter(props: { start: number }) {
const count = useSignal(props.start);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => count.value++}>+1</button>
<button onClick={() => count.value--}>-1</button>
</div>
);
}
Use it in a route — Fresh automatically bundles only this component:
// routes/index.tsx
import Counter from "../islands/Counter.tsx";
export default function Home() {
return (
<main>
<h1>Welcome</h1>
<Counter start={0} />
</main>
);
}
Middleware
// routes/_middleware.ts
import { FreshContext } from "$fresh/server.ts";
export async function handler(req: Request, ctx: FreshContext) {
const start = performance.now();
const resp = await ctx.next();
const duration = performance.now() - start;
resp.headers.set("X-Response-Time", `${duration.toFixed(1)}ms`);
// Authentication example
const url = new URL(req.url);
if (url.pathname.startsWith("/admin")) {
const token = req.headers.get("Authorization");
if (!token) {
return new Response("Unauthorized", { status: 401 });
}
}
return resp;
}
Layouts
// routes/_layout.tsx
import { PageProps } from "$fresh/server.ts";
import Header from "../components/Header.tsx";
export default function Layout({ Component }: PageProps) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="/styles.css" />
<title>My App</title>
</head>
<body>
<Header />
<main>
<Component />
</main>
</body>
</html>
);
}
API Routes
// routes/api/users.ts
import { Handlers } from "$fresh/server.ts";
export const handler: Handlers = {
async GET(_req, _ctx) {
const kv = await Deno.openKv();
const users = [];
for await (const entry of kv.list({ prefix: ["users"] })) {
users.push(entry.value);
}
return Response.json(users);
},
async POST(req, _ctx) {
const body = await req.json();
const kv = await Deno.openKv();
const id = crypto.randomUUID();
await kv.set(["users", id], { id, ...body });
return Response.json({ id }, { status: 201 });
},
};
Forms (Progressive Enhancement)
// routes/contact.tsx
import { Handlers, PageProps } from "$fresh/server.ts";
export const handler: Handlers = {
GET(_req, ctx) {
return ctx.render({ sent: false });
},
async POST(req, ctx) {
const form = await req.formData();
const email = form.get("email") as string;
const message = form.get("message") as string;
// process form...
return ctx.render({ sent: true });
},
};
export default function Contact({ data }: PageProps<{ sent: boolean }>) {
if (data.sent) return <p>Thanks for your message!</p>;
return (
<form method="POST">
<input type="email" name="email" required />
<textarea name="message" required />
<button type="submit">Send</button>
</form>
);
}
Core Philosophy
Fresh embodies the principle that the server should do as much work as possible and the browser should do as little as necessary. Pages are rendered entirely on the server using Preact JSX, and zero JavaScript is shipped to the client by default. Interactivity is added only when a component is explicitly placed in the islands/ directory, at which point Fresh bundles and hydrates just that component. This is the same islands architecture as Astro, but implemented with Preact and Deno rather than multi-framework support.
The file-system routing and handler pattern keep server-side concerns co-located with their routes. A route file exports a handler for data fetching and a default component for rendering, and the two are type-connected through PageProps. This co-location means you can read a single file and understand the entire data flow for a route: what the server fetches, how it is validated, and how it is rendered. There is no routing configuration file, no separate data layer, and no global state to trace.
Fresh is designed for Deno Deploy from the ground up. The handler model (Request -> Response) maps directly to Deploy's V8 isolate execution model. Deno KV provides built-in storage without a database connection string. Deno.cron provides scheduled tasks without an external scheduler. This tight integration means Fresh applications deploy to the edge with zero configuration, which makes it the natural choice for projects that target Deno's ecosystem.
Anti-Patterns
-
Placing components in
islands/that do not need client-side interactivity. Every island ships JavaScript to the browser. A card, header, or footer that is purely presentational should be a server component incomponents/, not an island. -
Passing non-serializable props to islands. Island props are serialized to JSON. Functions, class instances, Dates, and circular references will break serialization. Pass only plain data (strings, numbers, booleans, arrays, plain objects) as island props.
-
Building large, deeply nested islands. Wrapping an entire page section in an island ships all of its JavaScript to the client. Push interactivity to leaf-level components so that only the smallest necessary subtree is hydrated.
-
Fetching data inside components instead of handlers. Components render on the server, but data fetching logic belongs in the handler where it has access to
Request, context, and Deno APIs. Fetching inside components blurs the data flow and makes error handling harder. -
Using npm packages that access
windowordocumentat import time. Server-side rendering fails when a module tries to read browser globals during import. Guard browser-only code withIS_BROWSERfrom$fresh/runtime.tsor use dynamic imports inside islands.
Best Practices
- Default to server components — only promote to an island when the component needs browser interactivity (click handlers, client state, animations).
- Keep islands small and leaf-level — wrapping a large subtree in an island defeats the purpose. Push interactivity to the leaves.
- Use Preact Signals instead of
useStatein islands for fine-grained reactivity without re-renders. - Colocate data fetching in handlers — do not fetch data inside components. Handlers run server-side and can access KV, databases, and secrets safely.
- Use progressive enhancement — forms work without JavaScript via standard
<form method="POST">. Add islands only for richer UX. - Deploy to Deno Deploy for zero-config edge hosting; Fresh is designed for it.
Common Pitfalls
- Putting components in
islands/that don't need hydration — this ships unnecessary JavaScript. Only truly interactive components belong there. - Passing non-serializable props to islands — island props are serialized to JSON. Functions, classes, and circular references will break.
- Using Node/npm packages that access
windowat import time — server-side rendering will fail. Guard browser-only code withIS_BROWSERfrom$fresh/runtime.ts. - Forgetting that
_layout.tsxwraps all sibling and nested routes — unexpected layout nesting happens when layout files exist at multiple directory levels. - Large static assets in
static/— they are bundled into the deployment. Use an external CDN for large media files. - Misunderstanding route priority — explicit routes (e.g.,
blog/featured.tsx) take priority over dynamic routes (blog/[slug].tsx), but order-dependent conflicts can be subtle with multiple dynamic segments.
Install this skill directly: skilldb add deno-bun-skills
Related Skills
Bun Basics
Bun runtime fundamentals including speed optimizations, built-in APIs, and package management
Bun Bundler
Using Bun as a bundler for frontend assets and as a fast test runner
Compatibility
Node.js compatibility layers in Deno and Bun for running existing npm packages and Node APIs
Deno Basics
Deno runtime fundamentals including permissions, module system, and built-in tooling
Deno Deploy
Deno Deploy edge functions for globally distributed serverless applications
Elysia Bun
Elysia web framework on Bun for type-safe, high-performance HTTP APIs