Web Development

Deno Fresh: Web Framework Modern

Bangun website ultra-cepat dengan Deno Fresh — framework yang mengusung islands architecture, zero runtime JavaScript by default, edge-ready deployment, dan developer experience yang luar biasa.

1. Pengenalan Deno Fresh

Deno Fresh adalah full-stack web framework yang dibangun di atas Deno runtime. Fresh menggunakan pendekatan islands architecture di mana halaman dirender di server sebagai HTML statis, dan hanya komponen yang memerlukan interaktivitas yang dihidrasi di klien.

Filosofi utama Fresh adalah zero JavaScript by default. Setiap halaman dikirim sebagai HTML murni dari server. JavaScript hanya dikirim ke browser untuk komponen interaktif spesifik (islands). Ini menghasilkan website yang sangat cepat dan SEO-friendly.

Fitur Utama

FiturPenjelasanKeunggulan
Zero Runtime JSHTML statis, tanpa JS di klien kecuali islandsPerforma luar biasa
Islands ArchitectureHanya komponen interaktif yang dihidrasiBundle size minimal
Edge-ReadyDidesign untuk Deno Deploy (edge network)Latency rendah global
TypeScript FirstTypeScript native tanpa konfigurasiDX yang superior
Tailwind Built-inTailwind CSS tersedia out-of-the-boxTanpa konfigurasi
File-based RoutingSeperti Next.js/RemixKonvensi yang intuitif

Fresh vs Framework Lain

Diagram: Perbandingan Arsitektur
┌─────────────────────────────────────────────────────────────┐
│           PERBANDINGAN ARSITEKTUR FRAMEWORK WEB             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Traditional SPA (React/Next.js):                           │
│  Browser ──► Download JS Bundle ──► Hydrate ──► Render     │
│  ❌ JavaScript-heavy, slow initial load                     │
│                                                             │
│  Fresh (Islands Architecture):                              │
│  Browser ──► Server Render HTML ──► Display ──► Hydrate    │
│              (only islands get JS)    instantly   (small)   │
│  ✅ Fast FCP, minimal JS, excellent SEO                     │
│                                                             │
│  MPA (PHP/Rails):                                           │
│  Browser ──► Full page reload ──► Server render ──► Show   │
│  ✅ Simple but ❌ poor UX, no interactivity                 │
└─────────────────────────────────────────────────────────────┘
💡 Tips

Fresh ideal untuk website konten-heavy (blog, docs, marketing) di mana Anda memerlukan performa tinggi dengan sedikit interaktivitas klien. Untuk SPA yang sangat interaktif, framework seperti Next.js mungkin lebih sesuai.

2. Instalasi & Project Setup

Shell — Install Deno & Create Fresh Project
# Install Deno (jika belum terinstall)
# macOS / Linux
curl -fsSL https://deno.land/install.sh | sh

# Windows (PowerShell)
irm https://deno.land/install.ps1 | iex

# Verifikasi instalasi
deno --version
# deno 1.45.x

# Buat project Fresh baru
deno run -A -r https://fresh.deno.dev my-fresh-app

# Masuk ke direktori project
cd my-fresh-app

# Jalankan development server
deno task start

# Buka http://localhost:8000

Struktur Project

File Structure — Fresh Project
my-fresh-app/
├── components/          # Komponen reusable
│   ├── Button.tsx
│   └── Header.tsx
├── islands/             # ⭐ Islands (komponen interaktif)
│   ├── Counter.tsx
│   └── SearchBar.tsx
├── routes/              # 📁 File-based routing
│   ├── index.tsx        # Homepage → /
│   ├── about.tsx        # → /about
│   └── blog/
│       ├── index.tsx    # → /blog
│       └── [slug].tsx   # → /blog/:slug (dynamic)
├── static/              # 📦 Static assets (CSS, images)
│   ├── favicon.ico
│   └── styles.css
├── deno.json            # Deno configuration
├── dev.ts               # Development entry point
├── main.ts              # Production entry point
└── fresh.gen.ts         # Auto-generated manifest
JSON — deno.json Configuration
{
  "tasks": {
    "start": "deno run -A --watch=static/,routes dev.ts",
    "build": "deno run -A dev.ts build",
    "preview": "deno run -A main.ts"
  },
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "preact"
  },
  "imports": {
    "$fresh/": "https://deno.land/x/fresh@1.7.1/",
    "preact": "https://esm.sh/preact@10.24.3",
    "preact/": "https://esm.sh/preact@10.24.3/",
    "preact-render-to-string": "https://esm.sh/preact-render-to-string@6.5.11"
  }
}

3. Islands Architecture

Konsep Islands Architecture adalah inti dari Fresh. Bayangkan halaman web sebagai "lautan" HTML statis, dengan "pulau-pulau" (islands) interaktif yang dihidrasi secara mandiri di klien.

TSX — Island Component (islands/Counter.tsx)
// islands/Counter.tsx
// Komponen di folder islands/ otomatis dihidrasi di klien

import { useState } from "preact/hooks";

interface CounterProps {
  initialCount?: number;
  label?: string;
}

export default function Counter(
  { initialCount = 0, label = "Hitung" }: CounterProps,
) {
  const [count, setCount] = useState(initialCount);

  return (
    <div class="counter-island">
      <p class="counter-display">Count: {count}</p>
      <button
        class="btn-counter"
        onClick={() => setCount((c) => c - 1)}
      >
        −
      </button>
      <span class="counter-label">{label}</span>
      <button
        class="btn-counter"
        onClick={() => setCount((c) => c + 1)}
      >
        +
      </button>
    </div>
  );
}
TSX — Page yang Menggunakan Island
// routes/index.tsx
// Komponen biasa TIDAK dihidrasi (rendered sebagai HTML statis)
import Counter from "../islands/Counter.tsx";

export default function Home() {
  return (
    <div class="page">
      <h1>Selamat Datang di Fresh!</h1>
      <p>Paragraf ini adalah HTML statis — tanpa JavaScript.</p>

      {/*
        Counter ini ADALAH island —
        akan dihidrasi di klien dengan JavaScript
      */}
      <Counter initialCount={0} label="Klik saya" />

      <p>Paragraf ini juga statis — tidak ada JS yang dikirim.</p>
    </div>
  );
}

Visualisasi Islands

Diagram: Islands Rendering Flow
┌─────────────────────────────────────────────────────┐
│              ISLANDS RENDERING FLOW                  │
├─────────────────────────────────────────────────────┤
│                                                     │
│  Server-Side Render:                                │
│  ┌─────────────────────────────────────────────┐   │
│  │ [Header - statis]                           │   │
│  │ [Hero - statis]                             │   │
│  │ ┌─────────────────┐  [Sidebar - statis]    │   │
│  │ │ 🏝️ Counter (JS) │                        │   │
│  │ └─────────────────┘                        │   │
│  │ [Content - statis]                          │   │
│  │ ┌─────────────────┐  ┌─────────────────┐  │   │
│  │ │ 🏝️ SearchBar(JS)│  │ 🏝️ Cart (JS)    │  │   │
│  │ └─────────────────┘  └─────────────────┘  │   │
│  │ [Footer - statis]                           │   │
│  └─────────────────────────────────────────────┘   │
│                                                     │
│  Client-Side: Hanya 3 islands dihidrasi (kecil)     │
│  Total JS: ~15KB (vs ~200KB+ di SPA biasa)          │
└─────────────────────────────────────────────────────┘
TSX — Complex Island: SearchBar
// islands/SearchBar.tsx
import { useState, useEffect } from "preact/hooks";

interface SearchResult {
  title: string;
  url: string;
  snippet: string;
}

export default function SearchBar() {
  const [query, setQuery] = useState("");
  const [results, setResults] = useState<SearchResult[]>([]);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    if (query.length < 2) {
      setResults([]);
      return;
    }

    const controller = new AbortController();
    const timeoutId = setTimeout(async () => {
      setLoading(true);
      try {
        const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`, {
          signal: controller.signal,
        });
        if (res.ok) {
          setResults(await res.json());
        }
      } catch (e) {
        if (e instanceof DOMException && e.name === "AbortError") return;
        console.error("Search error:", e);
      }
      setLoading(false);
    }, 300);

    return () => {
      clearTimeout(timeoutId);
      controller.abort();
    };
  }, [query]);

  return (
    <div class="search-bar">
      <input
        type="text"
        value={query}
        onInput={(e) => setQuery(e.currentTarget.value)}
        placeholder="Cari artikel..."
        class="search-input"
      />
      {loading && <p class="search-loading">⏳ Mencari...</p>}
      {results.length > 0 && (
        <ul class="search-results">
          {results.map((r) => (
            <li key={r.url}>
              <a href={r.url}>{r.title}</a>
              <p>{r.snippet}</p>
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

4. Routing & Pages

Fresh menggunakan file-based routing — struktur file di folder routes/ langsung menentukan URL. Ini sama seperti pendekatan Next.js App Router.

TSX — Static Routes
// routes/index.tsx → /
export default function Home() {
  return (
    <div>
      <h1>Homepage</h1>
    </div>
  );
}

// routes/about.tsx → /about
export default function About() {
  return (
    <div>
      <h1>Tentang Kami</h1>
    </div>
  );
}

// routes/blog/index.tsx → /blog
export default function BlogList() {
  return (
    <div>
      <h1>Blog</h1>
    </div>
  );
}

// routes/blog/[slug].tsx → /blog/:slug (dynamic)
export default function BlogPost() {
  return (
    <div>
      <h1>Blog Post</h1>
    </div>
  );
}

Dynamic Routes & Route Parameters

TSX — Dynamic Route dengan Loader
// routes/blog/[slug].tsx
import { Handlers, PageProps } from "$fresh/server.ts";

interface Post {
  title: string;
  content: string;
  date: string;
}

// Handler: berjalan di server, mengambil data sebelum render
export const handler: Handlers<Post | null> = {
  async GET(_req, ctx) {
    const { slug } = ctx.params;  // Mengambil parameter dari URL
    const post = await fetchPostFromDB(slug);

    if (!post) {
      return ctx.render(null);  // Render 404
    }

    return ctx.render(post);    // Kirim data ke komponen
  },
};

// Komponen menerima data via props
export default function BlogPost({ data }: PageProps<Post | null>) {
  if (!data) {
    return (
      <div class="error-page">
        <h1>404 — Artikel Tidak Ditemukan</h1>
      </div>
    );
  }

  return (
    <article class="blog-post">
      <h1>{data.title}</h1>
      <time>{data.date}</time>
      <div dangerouslySetInnerHTML={{ __html: data.content }} />
    </article>
  );
}

// Helper function
async function fetchPostFromDB(slug: string): Promise<Post | null> {
  // Simulasi database query
  const posts: Record<string, Post> = {
    "hello-world": {
      title: "Hello World",
      content: "<p>Artikel pertama saya!</p>",
      date: "2026-06-29",
    },
  };
  return posts[slug] ?? null;
}

Layout & _app.tsx

TSX — Application Layout (_app.tsx)
// routes/_app.tsx — Layout wrapper untuk semua halaman
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>My Fresh App</title>
        <link rel="stylesheet" href="/styles.css" />
      </head>
      <body>
        <nav class="main-nav">
          <a href="/">Home</a>
          <a href="/blog">Blog</a>
          <a href="/about">About</a>
        </nav>
        <main>
          {/* Komponen halaman yang sedang aktif dirender di sini */}
          <Component />
        </main>
        <footer>
          <p>&copy; 2026 My Fresh App</p>
        </footer>
      </body>
    </html>
  );
}

5. Middleware

Fresh mendukung middleware yang berjalan sebelum handler dipanggil. Middleware berguna untuk logging, autentikasi, CORS, dan manipulasi request/response.

TypeScript — Middleware Logging
// routes/_middleware.ts — Berlaku untuk semua route
import { MiddlewareHandlerContext } from "$fresh/server.ts";

export async function handler(
  req: Request,
  ctx: MiddlewareHandlerContext,
) {
  const start = performance.now();
  const url = new URL(req.url);

  console.log(`→ ${req.method} ${url.pathname}`);

  // Lanjutkan ke handler berikutnya
  const resp = await ctx.next();

  const duration = (performance.now() - start).toFixed(2);
  console.log(`← ${resp.status} (${duration}ms)`);

  // Tambahkan header custom
  resp.headers.set("X-Response-Time", `${duration}ms`);
  resp.headers.set("X-Powered-By", "Deno Fresh");

  return resp;
}
TypeScript — Auth Middleware
// routes/dashboard/_middleware.ts — Hanya untuk /dashboard/*
import { MiddlewareHandlerContext } from "$fresh/server.ts";
import { getCookies } from "https://deno.land/std@0.224.0/http/cookie.ts";

export async function handler(
  req: Request,
  ctx: MiddlewareHandlerContext,
) {
  const cookies = getCookies(req.headers);
  const sessionToken = cookies["session"];

  if (!sessionToken) {
    // Redirect ke login jika tidak ada session
    const loginUrl = new URL("/login", req.url);
    loginUrl.searchParams.set("redirect", new URL(req.url).pathname);
    return Response.redirect(loginUrl.href, 302);
  }

  // Validasi session (contoh sederhana)
  const user = await validateSession(sessionToken);
  if (!user) {
    return new Response("Unauthorized", { status: 401 });
  }

  // Simpan user di state untuk diakses oleh handler
  ctx.state.user = user;

  const resp = await ctx.next();
  return resp;
}

async function validateSession(token: string) {
  // Validasi ke database atau session store
  return { id: "1", name: "John", email: "john@example.com" };
}

6. Data Fetching & Loaders

Fresh menggunakan handlers untuk mengambil data di server sebelum merender komponen. Data dikirim ke komponen sebagai props.

TSX — Data Fetching dengan Handler
// routes/products/index.tsx
import { Handlers, PageProps } from "$fresh/server.ts";

interface Product {
  id: number;
  name: string;
  price: number;
  image: string;
}

interface Data {
  products: Product[];
  total: number;
  page: number;
}

export const handler: Handlers<Data> = {
  async GET(req, ctx) {
    const url = new URL(req.url);
    const page = parseInt(url.searchParams.get("page") ?? "1");
    const limit = 12;
    const offset = (page - 1) * limit;

    // Fetch dari API/database
    const response = await fetch(
      `https://api.example.com/products?limit=${limit}&offset=${offset}`
    );
    const { products, total } = await response.json();

    return ctx.render({ products, total, page });
  },
};

export default function ProductsPage({ data }: PageProps<Data>) {
  const { products, total, page } = data;
  const totalPages = Math.ceil(total / 12);

  return (
    <div class="products-page">
      <h1>Produk ({total} item)</h1>
      <div class="product-grid">
        {products.map((p) => (
          <div key={p.id} class="product-card">
            <img src={p.image} alt={p.name} loading="lazy" />
            <h3>{p.name}</h3>
            <p>Rp {p.price.toLocaleString("id-ID")}</p>
          </div>
        ))}
      </div>
      <nav class="pagination">
        {page > 1 && (
          <a href={`/products?page=${page - 1}`}>← Sebelumnya</a>
        )}
        <span>Halaman {page} dari {totalPages}</span>
        {page < totalPages && (
          <a href={`/products?page=${page + 1}`}>Selanjutnya →</a>
        )}
      </nav>
    </div>
  );
}

API Routes

TypeScript — API Route Handler
// routes/api/search.ts → GET /api/search?q=query
import { Handlers } from "$fresh/server.ts";

export const handler: Handlers = {
  async GET(req) {
    const url = new URL(req.url);
    const query = url.searchParams.get("q") ?? "";

    if (query.length < 2) {
      return new Response(JSON.stringify([]), {
        headers: { "Content-Type": "application/json" },
      });
    }

    // Cari di database
    const results = await searchArticles(query);

    return new Response(JSON.stringify(results), {
      headers: { "Content-Type": "application/json" },
    });
  },

  async POST(req) {
    const body = await req.json();
    // Proses data...
    return new Response(JSON.stringify({ success: true }), {
      headers: { "Content-Type": "application/json" },
    });
  },
};

async function searchArticles(q: string) {
  return [
    { title: `Hasil untuk "${q}"`, url: "/blog/result", snippet: "..." },
  ];
}

7. Styling & Tailwind

Fresh mendukung Tailwind CSS secara native. Anda bisa langsung menggunakan class Tailwind di JSX tanpa konfigurasi tambahan.

TSX — Tailwind di Fresh
// routes/index.tsx dengan Tailwind CSS
import Counter from "../islands/Counter.tsx";

export default function Home() {
  return (
    <div class="min-h-screen bg-gray-950 text-white">
      <!-- Hero Section -->
      <section class="flex flex-col items-center justify-center py-20 px-4">
        <h1 class="text-5xl font-bold text-center mb-6 bg-gradient-to-r from-green-400 to-blue-500 bg-clip-text text-transparent">
          Welcome to Fresh
        </h1>
        <p class="text-xl text-gray-400 text-center max-w-2xl mb-10">
          Framework web ultra-cepat untuk Deno dengan islands architecture.
        </p>

        <!-- Island: interaktif -->
        <div class="p-8 bg-gray-900 rounded-2xl shadow-xl">
          <Counter initialCount={0} label="Mulai Menghitung" />
        </div>
      </section>

      <!-- Features Grid -->
      <section class="grid grid-cols-1 md:grid-cols-3 gap-6 max-w-6xl mx-auto px-4 pb-20">
        {[
          { icon: "⚡", title: "Zero JS", desc: "HTML statis, hanya islands yang punya JS" },
          { icon: "🏝️", title: "Islands", desc: "Arsitektur modern untuk performa optimal" },
          { icon: "🦕", title: "Deno", desc: "Runtime yang aman dan modern" },
        ].map((f) => (
          <div key={f.title} class="p-6 bg-gray-900 rounded-xl border border-gray-800 hover:border-green-500 transition">
            <div class="text-4xl mb-4">{f.icon}</div>
            <h3 class="text-xl font-semibold mb-2">{f.title}</h3>
            <p class="text-gray-400">{f.desc}</p>
          </div>
        ))}
      </section>
    </div>
  );
}

8. Deployment ke Deno Deploy

Deno Deploy adalah platform edge yang di-optimalkan untuk Deno. Fresh di-deploy ke Deno Deploy sangat mudah — cukup push ke GitHub dan hubungkan ke Deno Deploy.

Shell — Deploy ke Deno Deploy
# 1. Pastikan project sudah di-push ke GitHub
git add .
git commit -m "Initial commit"
git push origin main

# 2. Buka https://dash.deno.com
# 3. Klik "New Project"
# 4. Pilih repository GitHub Anda
# 5. Set entry point ke: main.ts
# 6. Deploy!

# Atau deploy manual dengan deployctl:
# Install deployctl
deno install -A deployctl

# Deploy langsung dari CLI
deployctl deploy --project=my-fresh-app main.ts

# Set environment variables
deployctl deploy --env=DATABASE_URL=postgres://... --project=my-app main.ts
YAML — GitHub Actions Auto-Deploy
# .github/workflows/deploy.yml
name: Deploy to Deno Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Deploy to Deno Deploy
        uses: denoland/deployctl@v1
        with:
          project: my-fresh-app
          entrypoint: main.ts
          root: .

9. Best Practices

✅ Fresh Best Practices
  • Minimalkan islands — hanya buat island untuk komponen yang benar-benar butuh interaktivitas klien
  • Gunakan components/ untuk komponen statis, islands/ untuk interaktif
  • Handler untuk data fetching — semua data harus diambil di server melalui handler
  • Error handling — buat custom error page (_500.tsx, _404.tsx)
  • Static assets — simpan di folder static/ dengan nama file yang mengandung hash
  • Tailwind CSS — manfaatkan built-in Tailwind untuk styling konsisten
  • SEO — karena Fresh SSR, semua konten sudah SEO-friendly secara default
  • Edge deployment — gunakan Deno Deploy untuk latency terendah
  • Testing — gunakan Deno test runner untuk unit dan integration test
  • Bundle analysis — periksa ukuran island JavaScript secara berkala

10. Quiz: Uji Pemahamanmu!

Setelah membaca tutorial Deno Fresh, jawablah 5 pertanyaan berikut:

Pertanyaan 1: Apa konsep utama yang digunakan Deno Fresh?

a) Micro Frontends
b) Islands Architecture
c) Serverless Functions
d) Microservices

Pertanyaan 2: Di folder mana komponen interaktif (island) ditempatkan?

a) routes/
b) components/
c) islands/
d) static/

Pertanyaan 3: Apa yang dilakukan handler dalam Fresh?

a) Meng-render komponen React
b) Mengambil data di server sebelum render
c) Mengelola state klien
d) Mengkompresi asset

Pertanyaan 4: Platform apa yang dioptimalkan untuk deploy Fresh?

a) Vercel
b) Netlify
c) Deno Deploy
d) AWS Lambda

Pertanyaan 5: Mengapa Fresh menghasilkan website yang sangat cepat?

a) Karena menggunakan framework JavaScript berat
b) Karena zero JavaScript by default dan hanya islands yang dihidrasi
c) Karena semua kode di-cache di browser
d) Karena menggunakan WebAssembly
🔍 Zoom
100%
🎨 Tema