Skip to main content
โšก Calmops

Fresh: The Full-Stack Web Framework for Deno

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