Deno is a secure JavaScript/TypeScript runtime with built-in TypeScript support, a URL-based module system, and permission-based security. Fresh is Deno’s full-stack web framework — it renders HTML on the server with no JavaScript by default, and “islands” opt into client-side interactivity only where needed. Deno Deploy hosts Fresh apps globally on CDN edge nodes. Claude Code generates Fresh routes, island components, Deno KV queries, and the deno.json task configuration.
CLAUDE.md for Fresh Projects
## Fresh Stack
- Deno 2.x + Fresh 2.x
- UI: Preact components (Fresh default) with TypeScript
- Styling: Tailwind CSS (Fresh plugin)
- Persistence: Deno KV (built-in key-value store)
- Deploy: Deno Deploy (zero config — push to GitHub → auto-deploy)
- Islands: client-interactive components only — everything else is server-rendered
- Auth: deno_kv_oauth (GitHub/Google OAuth via sessions in KV)
- No node_modules: all deps via JSR (jsr:) or esm.sh (https://)
Project Structure
fresh-project/
├── deno.json # Config + tasks + imports map
├── main.ts # Entry point
├── dev.ts # Dev server with HMR
├── fresh.config.ts # Fresh plugin config
├── routes/
│ ├── _app.tsx # Global layout
│ ├── _middleware.ts # Auth middleware
│ ├── index.tsx # Home page (/)
│ ├── orders/
│ │ ├── index.tsx # /orders
│ │ └── [id].tsx # /orders/:id
│ └── api/
│ └── orders.ts # /api/orders (REST endpoint)
├── islands/
│ ├── OrderForm.tsx # Interactive form (client JS)
│ └── SearchBar.tsx # Interactive search
└── components/
└── OrderCard.tsx # Server-rendered component
Fresh Route
// routes/orders/index.tsx — server-rendered orders list
import type { PageProps, Handlers } from '$fresh/server.ts';
import { OrderCard } from '../../components/OrderCard.tsx';
import SearchBar from '../../islands/SearchBar.tsx';
import { db } from '../../lib/db.ts';
interface Order {
id: string;
customerName: string;
status: string;
totalCents: number;
createdAt: string;
}
interface PageData {
orders: Order[];
query: string;
}
// Server-side handler: runs on every request, never sent to client
export const handler: Handlers<PageData> = {
async GET(req, ctx) {
const url = new URL(req.url);
const query = url.searchParams.get('q') ?? '';
const orders = await db.orders.list(query);
return ctx.render({ orders, query });
},
async POST(req) {
const body = await req.formData();
const orderId = body.get('cancel_order_id') as string;
if (orderId) {
await db.orders.cancel(orderId);
}
return new Response(null, {
status: 303,
headers: { Location: '/orders' },
});
},
};
export default function OrdersPage({ data }: PageProps<PageData>) {
const { orders, query } = data;
return (
<div class="container mx-auto p-6">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold">Orders</h1>
<a href="/orders/new" class="btn btn-primary">New Order</a>
</div>
{/* Island: only this component has client-side JS */}
<SearchBar initialQuery={query} />
{orders.length === 0 ? (
<p class="text-center text-gray-500 py-12">No orders found.</p>
) : (
<div class="grid gap-4">
{orders.map(order => (
<OrderCard key={order.id} order={order} />
))}
</div>
)}
</div>
);
}
Islands (Client-Side Interactivity)
// islands/SearchBar.tsx — client-side search with debounce
import { useSignal, useComputed } from '@preact/signals';
import { useEffect } from 'preact/hooks';
interface SearchBarProps {
initialQuery: string;
}
export default function SearchBar({ initialQuery }: SearchBarProps) {
const query = useSignal(initialQuery);
// Debounced navigation
useEffect(() => {
const timeoutId = setTimeout(() => {
const url = new URL(window.location.href);
if (query.value) {
url.searchParams.set('q', query.value);
} else {
url.searchParams.delete('q');
}
// Use replaceState for debounced search (don't add to history)
window.history.replaceState({}, '', url.toString());
// Reload to trigger server-side search
window.location.reload();
}, 300);
return () => clearTimeout(timeoutId);
}, [query.value]);
return (
<div class="relative mb-6">
<input
type="search"
value={query.value}
onInput={(e) => query.value = (e.target as HTMLInputElement).value}
placeholder="Search orders..."
class="w-full px-4 py-2 pl-10 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
aria-label="Search orders"
/>
<svg
class="absolute left-3 top-2.5 h-5 w-5 text-gray-400"
fill="none" stroke="currentColor" viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
);
}
// islands/OrderForm.tsx — interactive order creation
import { useSignal } from '@preact/signals';
import { IS_BROWSER } from '$fresh/runtime.ts';
interface OrderItem {
productId: string;
quantity: number;
}
export default function OrderForm() {
const items = useSignal<OrderItem[]>([{ productId: '', quantity: 1 }]);
const isSubmitting = useSignal(false);
const error = useSignal('');
const addItem = () => {
items.value = [...items.value, { productId: '', quantity: 1 }];
};
const removeItem = (index: number) => {
items.value = items.value.filter((_, i) => i !== index);
};
const handleSubmit = async (e: Event) => {
e.preventDefault();
isSubmitting.value = true;
error.value = '';
try {
const formData = new FormData(e.target as HTMLFormElement);
const response = await fetch('/api/orders', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
customerId: formData.get('customerId'),
items: items.value.filter(i => i.productId),
}),
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.error ?? 'Failed to create order');
}
const order = await response.json();
window.location.href = `/orders/${order.id}`;
} catch (e: any) {
error.value = e.message;
} finally {
isSubmitting.value = false;
}
};
if (!IS_BROWSER) {
return <div>Loading form...</div>;
}
return (
<form onSubmit={handleSubmit} class="space-y-4">
{error.value && (
<div class="bg-red-50 text-red-700 p-3 rounded">{error.value}</div>
)}
<input name="customerId" type="text" placeholder="Customer ID" required
class="w-full border rounded px-3 py-2" />
<div class="space-y-2">
{items.value.map((item, index) => (
<div key={index} class="flex gap-2">
<input
type="text"
placeholder="Product ID"
value={item.productId}
onInput={(e) => {
const newItems = [...items.value];
newItems[index].productId = (e.target as HTMLInputElement).value;
items.value = newItems;
}}
class="flex-1 border rounded px-3 py-2"
/>
<input type="number" min="1" max="100"
value={item.quantity}
onInput={(e) => {
const newItems = [...items.value];
newItems[index].quantity = parseInt((e.target as HTMLInputElement).value) || 1;
items.value = newItems;
}}
class="w-20 border rounded px-3 py-2" />
{items.value.length > 1 && (
<button type="button" onClick={() => removeItem(index)}
class="text-red-500 px-2">✕</button>
)}
</div>
))}
<button type="button" onClick={addItem}
class="text-blue-600 text-sm">+ Add item</button>
</div>
<button type="submit" disabled={isSubmitting.value}
class="w-full btn btn-primary">
{isSubmitting.value ? 'Creating...' : 'Create Order'}
</button>
</form>
);
}
Deno KV Persistence
// lib/db.ts — Deno KV database layer
const kv = await Deno.openKv();
export const db = {
orders: {
async list(query = ''): Promise<Order[]> {
const entries = kv.list<Order>({ prefix: ['orders'] });
const orders: Order[] = [];
for await (const entry of entries) {
const order = entry.value;
if (query === '' ||
order.customerName.toLowerCase().includes(query.toLowerCase()) ||
order.id.includes(query)) {
orders.push(order);
}
}
return orders.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
},
async get(id: string): Promise<Order | null> {
const result = await kv.get<Order>(['orders', id]);
return result.value;
},
async create(data: Omit<Order, 'id' | 'createdAt'>): Promise<Order> {
const id = crypto.randomUUID();
const order: Order = { ...data, id, createdAt: new Date().toISOString() };
await kv.set(['orders', id], order);
return order;
},
async cancel(id: string): Promise<void> {
const order = await this.get(id);
if (order) {
await kv.set(['orders', id], { ...order, status: 'cancelled' });
}
},
},
};
deno.json Configuration
{
"imports": {
"$fresh/": "https://deno.land/x/[email protected]/",
"preact": "https://esm.sh/[email protected]",
"@preact/signals": "https://esm.sh/@preact/[email protected]"
},
"tasks": {
"check": "deno check **/*.ts **/*.tsx",
"start": "deno run -A --watch=static/,routes/ dev.ts",
"build": "deno run -A dev.ts build",
"preview": "deno run -A main.ts"
},
"lint": { "rules": { "tags": ["fresh", "recommended"] } },
"fmt": { "indentWidth": 4, "singleQuote": true }
}
For the Bun runtime alternative to Deno that also provides built-in TypeScript support, see the Bun runtime guide for Bun.serve(), bun:sqlite, and the Bun test runner. For SvelteKit as a full-stack alternative framework with similar SSR capabilities, the SvelteKit guide covers form actions and server-side rendering. The Claude Skills 360 bundle includes Deno/Fresh skill sets covering island architecture, Deno KV, and Deno Deploy configuration. Start with the free tier to try Fresh route generation.