Deno 2 Fresh Framework Full-Stack Guide
Deno Fresh framework takes a radically different approach to web development. Instead of shipping megabytes of JavaScript to the browser, Fresh renders everything on the server and only hydrates interactive “islands” of UI. Combined with Deno 2’s native TypeScript support, npm compatibility, and zero-config deployment, Fresh offers one of the most productive full-stack development experiences available in 2026.
This guide covers building a real application with Fresh from scratch — routing, layouts, islands, forms, database integration, and deployment. You will see how Fresh eliminates the build step entirely while delivering exceptional performance out of the box.
Why Fresh in 2026
The JavaScript ecosystem has been moving toward server-first rendering. Next.js has Server Components, Remix has loaders, and Astro has content-first rendering. Fresh takes this further by sending zero JavaScript by default. Every page is server-rendered HTML. Interactive components must be explicitly opted in as “islands,” which means you consciously choose what ships to the client.
The result is dramatically smaller page weights. A typical Fresh page sends 0-5 KB of JavaScript compared to 200+ KB for a comparable React SPA. Moreover, Fresh uses Preact (3 KB) instead of React (40 KB) for islands, keeping even interactive pages lightweight.
Setting Up a Fresh Project
With Deno 2 installed, creating a new Fresh project takes a single command. No node_modules, no package.json, no build configuration:
# Install Deno 2 (if not already installed)
curl -fsSL https://deno.land/install.sh | sh
# Create a new Fresh project
deno run -A -r https://fresh.deno.dev my-app
cd my-app
# Start the dev server (no build step!)
deno task startThe project structure follows file-system routing similar to Next.js:
my-app/
├── routes/
│ ├── index.tsx # / route
│ ├── about.tsx # /about route
│ ├── api/
│ │ └── posts.ts # /api/posts API route
│ ├── blog/
│ │ ├── index.tsx # /blog route
│ │ └── [slug].tsx # /blog/:slug dynamic route
│ └── _layout.tsx # Shared layout
├── islands/
│ ├── Counter.tsx # Interactive island component
│ └── SearchBar.tsx # Client-side search
├── components/
│ ├── Header.tsx # Server-only component
│ └── Footer.tsx # Server-only component
├── static/
│ └── styles.css
├── fresh.config.ts
└── deno.jsonRoutes and Server-Side Rendering
Every file in the routes/ directory becomes a route. The Deno Fresh framework uses handler functions for data loading and component functions for rendering — similar to Remix loaders:
// routes/blog/[slug].tsx
import { Handlers, PageProps } from "$fresh/server.ts";
import { db } from "../../utils/db.ts";
interface BlogPost {
title: string;
content: string;
publishedAt: Date;
author: string;
}
export const handler: Handlers = {
async GET(_req, ctx) {
const post = await db.query(
"SELECT * FROM posts WHERE slug = $1",
[ctx.params.slug]
);
if (!post) {
return ctx.renderNotFound();
}
return ctx.render(post);
},
};
export default function BlogPostPage({ data }: PageProps) {
return (
{data.title}
By {data.author} on {new Date(data.publishedAt).toLocaleDateString()}
);
} Islands: Selective Client-Side Interactivity
Islands are the core innovation of Fresh. Any component placed in the islands/ directory gets hydrated on the client. Everything else stays as static HTML. This is how you add interactivity without shipping a full framework to the browser:
// islands/SearchBar.tsx
import { useSignal } from "@preact/signals";
interface SearchResult {
title: string;
slug: string;
excerpt: string;
}
export default function SearchBar() {
const query = useSignal("");
const results = useSignal([]);
const isLoading = useSignal(false);
const handleSearch = async (value: string) => {
query.value = value;
if (value.length < 2) {
results.value = [];
return;
}
isLoading.value = true;
const res = await fetch(
`/api/search?q=${encodeURIComponent(value)}`
);
results.value = await res.json();
isLoading.value = false;
};
return (
handleSearch(e.currentTarget.value)}
class="w-full px-4 py-2 border rounded-lg"
/>
{results.value.length > 0 && (
{results.value.map((r) => (
-
{r.title}
{r.excerpt}
))}
)}
);
} Use the island in any route by importing it like a regular component:
// routes/index.tsx
import SearchBar from "../islands/SearchBar.tsx";
import Header from "../components/Header.tsx";
export default function Home() {
return (
{/* Server-rendered, zero JS */}
{/* Hydrated island, ships JS */}
{/* Server-rendered, zero JS */}
);
}Forms and Mutations
Fresh handles form submissions server-side, similar to traditional web frameworks. No client-side JavaScript needed for forms:
// routes/contact.tsx
import { Handlers, PageProps } from "$fresh/server.ts";
interface FormState {
success?: boolean;
error?: string;
}
export const handler: Handlers = {
GET(_req, ctx) {
return ctx.render({});
},
async POST(req, ctx) {
const form = await req.formData();
const name = form.get("name")?.toString();
const email = form.get("email")?.toString();
const message = form.get("message")?.toString();
if (!name || !email || !message) {
return ctx.render({ error: "All fields are required" });
}
await db.query(
"INSERT INTO contacts (name, email, message) VALUES ($1, $2, $3)",
[name, email, message]
);
return ctx.render({ success: true });
},
};
export default function Contact({ data }: PageProps) {
return (
);
} When NOT to Use Fresh
Fresh is not the right choice for heavily interactive single-page applications where most of the page is dynamic — dashboards, real-time collaboration tools, or design editors. In these cases, the island architecture creates too many hydration boundaries, and a traditional SPA framework like React or Solid would be more ergonomic.
Additionally, if your team is deeply invested in the React ecosystem (component libraries, state management, testing tools), switching to Preact-based islands means losing access to some React-specific libraries. The npm compatibility in Deno 2 helps, but not all React packages work with Preact.
Key Takeaways
- The Deno Fresh framework ships zero JavaScript by default, hydrating only interactive islands on the client
- File-system routing with handler functions provides a clean separation between data loading and rendering
- Islands use Preact (3 KB) instead of React, keeping bundle sizes minimal even for interactive components
- Forms work without JavaScript using standard POST handlers — just like traditional web frameworks
- Fresh eliminates the build step entirely, going from TypeScript source to production without webpack, Vite, or any bundler