Fresh Core Knowledge
Full Reference: See advanced.md for Shared Signals, Middleware (Auth, CORS), Plugins, Error Handling, and Production Readiness.
Basic Setup
Create new Fresh project
deno run -A -r https://fresh.deno.dev my-project
Project structure
my-project/ ├── deno.json ├── dev.ts ├── main.ts ├── fresh.gen.ts # Auto-generated manifest ├── routes/ # File-based routing ├── islands/ # Interactive components ├── components/ # Static components └── static/ # Static assets
Configuration
// deno.json { "tasks": { "start": "deno run -A --watch=static/,routes/ dev.ts", "build": "deno run -A dev.ts build", "preview": "deno run -A main.ts" }, "imports": { "$fresh/": "https://deno.land/x/fresh@1.6.8/", "preact": "https://esm.sh/preact@10.19.6", "@preact/signals": "https://esm.sh/*@preact/signals@1.2.2" }, "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "preact" } }
File-Based Routing
// routes/index.tsx export default function Home() { return <h1>Welcome to Fresh</h1>; }
// routes/users/[id].tsx - Dynamic route import { PageProps } from "$fresh/server.ts";
export default function UserPage(props: PageProps) { const { id } = props.params; return <h1>User: {id}</h1>; }
// routes/blog/[...slug].tsx - Catch-all export default function BlogPost(props: PageProps) { const { slug } = props.params; return <h1>Post: {slug}</h1>; }
// routes/(marketing)/pricing.tsx - Route groups // URL: /pricing (group name not in URL)
Handlers
// routes/api/users.ts import { Handlers } from "$fresh/server.ts";
export const handler: Handlers = { GET(_req, _ctx) { const users = [{ id: 1, name: "Alice" }]; return new Response(JSON.stringify(users), { headers: { "Content-Type": "application/json" }, }); },
async POST(req, ctx) { const body = await req.json(); // Process... return new Response(JSON.stringify(body), { status: 201 }); }, };
Handler with Page
// routes/greet/[name].tsx import { Handlers, PageProps } from "$fresh/server.ts";
interface Data { greeting: string; }
export const handler: Handlers<Data> = {
GET(_req, ctx) {
return ctx.render({ greeting: Hello, ${ctx.params.name}! });
},
};
export default function GreetPage({ data }: PageProps<Data>) { return <h1>{data.greeting}</h1>; }
Islands Architecture
// islands/Counter.tsx import { useSignal } from "@preact/signals";
export default function Counter() { const count = useSignal(0);
return ( <div> <p>Count: {count.value}</p> <button onClick={() => count.value++}>Increment</button> </div> ); }
// routes/index.tsx - Using islands import Counter from "../islands/Counter.tsx";
export default function Home() { return ( <div> <h1>My App</h1> {/* This component is hydrated on the client */} <Counter /> </div> ); }
Islands with Props
// islands/Greeting.tsx import { useSignal } from "@preact/signals";
interface GreetingProps { initialName: string; }
export default function Greeting({ initialName }: GreetingProps) { const name = useSignal(initialName);
return ( <div> <input type="text" value={name.value} onInput={(e) => name.value = (e.target as HTMLInputElement).value} /> <p>Hello, {name.value}!</p> </div> ); }
Signals (State Management)
// islands/TodoList.tsx import { useSignal, useComputed } from "@preact/signals";
export default function TodoList() { const todos = useSignal<{ id: number; text: string; done: boolean }[]>([]); const newTodo = useSignal("");
const remaining = useComputed(() => todos.value.filter((t) => !t.done).length );
const addTodo = () => { if (newTodo.value.trim()) { todos.value = [...todos.value, { id: Date.now(), text: newTodo.value, done: false, }]; newTodo.value = ""; } };
return ( <div> <input value={newTodo.value} onInput={(e) => newTodo.value = (e.target as HTMLInputElement).value} onKeyDown={(e) => e.key === "Enter" && addTodo()} /> <button onClick={addTodo}>Add</button> <p>{remaining.value} items remaining</p> </div> ); }
Static Components
// components/Header.tsx import { JSX } from "preact";
interface HeaderProps { title: string; children?: JSX.Element; }
export function Header({ title, children }: HeaderProps) { return ( <header> <h1>{title}</h1> <nav>{children}</nav> </header> ); }
Forms
// routes/contact.tsx import { Handlers, PageProps } from "$fresh/server.ts";
interface PageData { error?: string; success?: boolean; }
export const handler: Handlers<PageData> = { 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();
if (!name || !email) {
return ctx.render({ error: "All fields required" });
}
return ctx.render({ success: true });
}, };
export default function ContactPage({ data }: PageProps<PageData>) { return ( <div> {data.error && <p style={{ color: "red" }}>{data.error}</p>} {data.success && <p style={{ color: "green" }}>Sent!</p>}
<form method="POST">
<input type="text" name="name" required />
<input type="email" name="email" required />
<button type="submit">Send</button>
</form>
</div>
); }
Basic Middleware
// routes/_middleware.ts import { FreshContext } from "$fresh/server.ts";
export async function handler(req: Request, ctx: FreshContext) {
const start = performance.now();
const resp = await ctx.next();
const duration = performance.now() - start;
resp.headers.set("X-Response-Time", ${duration.toFixed(2)}ms);
return resp;
}
Checklist
-
Islands only for interactive components
-
Static components for server-rendered content
-
Middleware for cross-cutting concerns
-
Error pages (_404.tsx, _500.tsx)
-
Health/readiness endpoints
-
Environment variables via Deno.env
-
Form validation on both client and server
When NOT to Use This Skill
-
Node.js Projects: Use Next.js, Remix
-
Pure API Servers: Use Oak for Deno API-only apps
-
Edge Runtimes: Use Hono for Cloudflare Workers
-
Static Sites: Use Astro or other SSG tools
Anti-Patterns
Anti-Pattern Why It's Bad Correct Approach
Using islands for static content Unnecessary hydration Use static components
Heavy computation in islands Slow client-side Compute on server
Fetching data in islands Waterfalls Fetch in handlers
Hardcoding env variables Not portable Use Deno.env.get()
Quick Troubleshooting
Issue Solution
Island not hydrating Move component to islands/ folder
Props not passing Ensure props are JSON-serializable
Handler not executing Ensure handler is exported
404 for dynamic route Use [param].tsx naming
Signals not reactive Access with .value
Reference Documentation
-
Routing
-
Handlers
-
Islands