Deno 2 Fresh Framework: Building Full-Stack Web Applications

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.

Deno Fresh framework web development
Fresh island architecture delivering zero-JS pages with selective hydration

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 start

The 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.json

Routes 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 && ( )}
); }

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 */}
); }
Full-stack web development with Deno
Building interactive islands within server-rendered Fresh pages

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 (
    
{data.success &&

Message sent!

} {data.error &&

{data.error}

}

Scroll to Top