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
| Fitur | Penjelasan | Keunggulan |
|---|---|---|
| Zero Runtime JS | HTML statis, tanpa JS di klien kecuali islands | Performa luar biasa |
| Islands Architecture | Hanya komponen interaktif yang dihidrasi | Bundle size minimal |
| Edge-Ready | Didesign untuk Deno Deploy (edge network) | Latency rendah global |
| TypeScript First | TypeScript native tanpa konfigurasi | DX yang superior |
| Tailwind Built-in | Tailwind CSS tersedia out-of-the-box | Tanpa konfigurasi |
| File-based Routing | Seperti Next.js/Remix | Konvensi yang intuitif |
Fresh vs Framework Lain
┌─────────────────────────────────────────────────────────────┐ │ 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 │ └─────────────────────────────────────────────────────────────┘
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
# 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
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
{
"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.
// 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>
);
}
// 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
┌─────────────────────────────────────────────────────┐ │ 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) │ └─────────────────────────────────────────────────────┘
// 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.
// 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
// 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
// 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>© 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.
// 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;
}
// 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.
// 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
// 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.
// 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.
# 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
# .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
- 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: