Generated banner for Deno Frontend
Generated logo for Deno Frontend

Deno Frontend

DenoOfficialCreated by denolandDistributed by Deno

  • Skill
  • Tool integration
  • Software development
  • MIT
  • No issues found

From the author

Use when building a web frontend with Deno — running React, Vite, Astro, SvelteKit, Next.js, Nuxt or other npm frameworks under Deno, or working with Fresh, Deno's own island-architecture framework. Covers which path to pick, Fresh 2.x routes, handlers, islands, Preact signals, Tailwind, and Fresh 1.x to 2.x migration.
— the description by denoland

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

This skill provides guidance on developing web frontends within the Deno ecosystem. It distinguishes between two primary approaches: using standard npm-based frameworks (like React, Vite, Astro, or Next.js) which run natively in Deno, and using Fresh, Deno's own island-architecture framework. The documentation covers project setup, configuration, and migration paths for Fresh 2.x, including details on routing, handlers, and state management.

What it does

  • Explains how to run popular npm frontend frameworks using Deno's native task runner.
  • Provides setup instructions for Fresh, Deno's server-side rendering framework.
  • Details the project structure for Fresh 2.x applications.
  • Outlines the migration process from Fresh 1.x to 2.x.
  • Describes how to handle routing, data fetching, and state in Fresh.
  • Offers configuration tips for integrating tools like Tailwind CSS with Deno.

When to use it

  • When starting a new web project and deciding between standard npm frameworks or Deno's Fresh.
  • When you need to run existing React, Vite, or Next.js projects using Deno.
  • When building a new Deno-first application that requires server-side rendering.
  • When upgrading an existing Fresh 1.x project to the 2.x version.
  • When you need to understand how to structure routes, islands, and handlers in Fresh.

Advantages

  • Seamless integration with the existing npm frontend ecosystem.
  • Minimal client-side JavaScript overhead via Fresh's island architecture.
  • Clear guidance on project configuration and dependency management.
  • Automated migration tools for upgrading legacy Fresh projects.
  • Consistent patterns for server-side data handling and routing.

Usage examples

“How do I start a new Vite project using Deno?”
You can use 'deno create vite my-app', followed by 'deno install' and 'deno task dev' to run the project.
“How do I migrate my Fresh 1.x project to 2.x?”
Run 'deno run -Ar jsr:@fresh/update' to automatically convert imports, update handler signatures, and switch to the Vite-based build system.

Install

  1. Download the skill as a ZIP:

    https://mp-staging.autogineer.com/agent-skills/denoland/deno-frontend/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip deno-frontend.zip -d .agents/skills

Documentation by denoland

Frontend development with Deno

Two paths. Pick by what the project already uses.

Regular npm frameworks — the usual choice

Deno runs the normal frontend ecosystem: React, Vue, Svelte, Solid, Vite, Astro, Next.js, Nuxt, SvelteKit, Remix, SolidStart. Nothing needs to be ported, and there is no Deno-specific way to write them.

deno create vite my-app     # or astro, next, nuxt, svelte…
cd my-app
deno install
deno task dev

deno create is npm create. deno install reads package.json. deno task <script> runs package.json scripts. That is the whole difference.

Follow the framework's own documentation for everything else — routing, components, data loading, and config are the framework's concern, not Deno's. Don't invent Deno-flavoured variants of their APIs, and don't reach for Fresh patterns in a React or Svelte project.

The only things worth knowing:

  • Some frameworks need "nodeModulesDir": "auto" in deno.json; Next.js does.

  • Permissions apply to the dev server too — framework tasks generally want -A.

  • Deploying: see the deno-deploy skill, which lists per-framework build commands and presets.

Fresh — Deno's own framework

Fresh suits a new Deno-first project that wants server rendering with minimal client JavaScript. It uses island architecture: pages render on the server, and only components in islands/ ship JavaScript.

deno run -Ar jsr:@fresh/init
cd my-project
deno task dev                # http://localhost:5173

A Fresh project is Vite-based: vite.config.ts for the build, main.ts for the server, file-based routes in routes/, islands in islands/, server-only components in components/. Preact supplies the component model, with signals for state.

// islands/Counter.tsx — interactive, ships JS
import { useSignal } from "@preact/signals";

export default function Counter() {
  const count = useSignal(0);
  return <button onClick={() => count.value++}>Count: {count.value}</button>;
}

Three rules carry most of the weight:

  1. Islands ship JavaScript — keep them small, leave everything else in components/.

  2. Island props must be serializable. Functions cannot be passed.

  3. Handlers take a single (ctx) parameter.

Load references/FRESH.md for routes, handlers, define helpers, middleware, islands, signals, and Tailwind.

Use Fresh 2.x, imported from "fresh". For an existing 1.x project, or one pinned to a 2.0.0-alpha.* release, see references/FRESH_MIGRATION.md.

Further reading

  • https://fresh.deno.dev/docs — Fresh documentation

  • references/FRESH.md — Fresh 2.x patterns in depth

  • references/FRESH_MIGRATION.md — Fresh 1.x and alpha migration

Files

references/FRESH.md

Fresh 2.x

Docs: https://fresh.deno.dev/docs. All imports come from "fresh" or "fresh/runtime".

Project layout

my-project/
├── deno.json           # config, dependencies, tasks
├── main.ts             # server entry point
├── client.ts           # client entry point (CSS imports)
├── vite.config.ts      # Vite configuration
├── routes/             # pages and API routes
│   ├── _app.tsx        # outer HTML wrapper
│   ├── _layout.tsx     # optional nested layout
│   ├── _error.tsx      # unified error page (404 and 500)
│   ├── _middleware.ts
│   └── index.tsx       # /
├── islands/            # interactive, hydrated on the client
├── components/         # server-only, ships no JS
├── static/
└── utils/state.ts      # define helpers

There is no manifest file, no separate dev entry point, and no separate config file.

main.ts
import { App, fsRoutes, staticFiles, trailingSlashes } from "fresh";

const app = new App()
  .use(staticFiles())
  .use(trailingSlashes("never"));

await fsRoutes(app, {
  dir: "./",
  loadIsland: (path) => import(`./islands/${path}`),
  loadRoute: (path) => import(`./routes/${path}`),
});

if (import.meta.main) {
  await app.listen();
}
vite.config.ts
import { defineConfig } from "vite";
import { fresh } from "@fresh/plugin-vite";
import tailwindcss from "@tailwindcss/vite";

export default defineConfig({
  plugins: [fresh(), tailwindcss()],
});
deno.json
{
  "tasks": {
    "dev": "vite",
    "build": "vite build",
    "preview": "deno serve -A _fresh/server.js"
  },
  "imports": {
    "fresh": "jsr:@fresh/core@^2",
    "fresh/runtime": "jsr:@fresh/core@^2/runtime",
    "@fresh/plugin-vite": "jsr:@fresh/plugin-vite@^1",
    "@preact/signals": "npm:@preact/signals@^2",
    "preact": "npm:preact@^10",
    "preact/hooks": "npm:preact@^10/hooks",
    "@/": "./"
  }
}
Imports
import { App, fsRoutes, staticFiles } from "fresh";
import { cors, csp, trailingSlashes } from "fresh";
import { createDefine, HttpError } from "fresh";
import type { Middleware, PageProps, RouteConfig } from "fresh";
import { IS_BROWSER } from "fresh/runtime";
import { computed, signal, useSignal } from "@preact/signals";
import { useEffect, useRef, useState } from "preact/hooks";

Routing

File-based, from routes/:

FileMatches
about.tsx/about
blog/[slug].tsx/blog/my-post
docs/[[version]].tsx/docs and /docs/v2
old/[...path].tsx/old/foo/bar
(marketing)/shared layout, no URL segment

_app.tsx wraps every page:

import type { PageProps } from "fresh";

export default function App({ Component }: PageProps) {
  return (
    <html>
      <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <title>My App</title>
      </head>
      <body>
        <Component />
      </body>
    </html>
  );
}

Define helpers

One-time setup giving handlers and pages a shared typed state:

// utils/state.ts
import { createDefine } from "fresh";

export interface State {
  user?: { id: string; name: string };
}

export const define = createDefine<State>();

Bare handler exports work but lose type safety — prefer define.handlers().

Data fetching

Handler returning { data } — the default. Supports auth checks and redirects before render:

// routes/posts.tsx
import { define } from "@/utils/state.ts";

export const handler = define.handlers(async (ctx) => {
  const res = await fetch("https://example.com/posts");
  return { data: { posts: await res.json() } };
});

export default define.page<typeof handler>(({ data }) => (
  <ul>{data.posts.map((p) => <li key={p.id}>{p.title}</li>)}</ul>
));

Async page component — fine when there is no auth or redirect logic:

export default async function ServersPage() {
  const servers = await db.query("SELECT * FROM servers");
  return <ul>{servers.map((s) => <li key={s.id}>{s.name}</li>)}</ul>;
}

Handlers

Single (ctx) parameter, one function or per-method:

export const handler = define.handlers({
  GET(ctx) {
    return Response.json({ users: [] });
  },
  async POST(ctx) {
    const body = await ctx.req.json();
    return Response.json({ created: true }, { status: 201 });
  },
});

The context object:

PropertyIs
ctx.reqthe Request
ctx.urlURL with pathname, searchParams
ctx.paramsroute parameters
ctx.staterequest-scoped data shared with middleware
ctx.configFresh configuration
ctx.routematched route pattern
ctx.errorcaught error, on error pages

Methods: ctx.render(<JSX />, { status, headers }) (JSX only, not data objects), ctx.redirect("/other", 301), ctx.next().

Throw HttpError to reach _error.tsx:

import { HttpError } from "fresh";

if (!post) throw new HttpError(404);
if (!ctx.state.user) throw new HttpError(401);

Middleware

// routes/_middleware.ts
import { define } from "@/utils/state.ts";

export const handler = define.middleware(async (ctx) => {
  console.log(`${ctx.req.method} ${ctx.url.pathname}`);
  const response = await ctx.next();
  return response;
});

Islands

Components in islands/ (or (_islands)/ inside routes) hydrate on the client. Use them for interactions, client state, and browser APIs — nothing else.

Props must be serializable: primitives, Infinity/NaN/-0, Array, Map, Set, plain objects, URL, Date, RegExp, Uint8Array, JSX elements, signals, and circular references. Functions cannot be passed.

For code that must not run during SSR:

import { IS_BROWSER } from "fresh/runtime";

export default function LocalStorageCounter() {
  if (!IS_BROWSER) return <div>Loading...</div>;
  const count = useSignal(Number(localStorage.getItem("count") ?? 0));
  return <button onClick={() => count.value++}>Count: {count.value}</button>;
}

Preact

Preact is a 3KB React alternative. Hooks behave as in React; class and className both work, class is the convention.

Signals are the preferred state primitive — granular updates, definable outside a component:

import { computed, signal } from "@preact/signals";

const count = signal(0);
const doubled = computed(() => count.value * 2);

Inside a component use useSignal(0).

Tailwind

Optional. Fresh builds with Vite, so Tailwind attaches as a Vite plugin. Install both packages — the plugin and the core library:

deno add npm:@tailwindcss/vite npm:tailwindcss

Add tailwindcss() to vite.config.ts plugins, put @import "tailwindcss"; in a CSS file (assets/styles.css by convention), and import that file from client.ts. Prefer utility classes over @apply; use the class strategy for dark mode.

Build and deploy

deno task dev        # dev server, port 5173
deno task build      # production build — required before deploying
deno task preview    # preview the build locally
deno deploy --prod

The build step is not optional; deploying without it ships no compiled output.

Requirements and compatibility

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Security

No issues found by our static scan· No issues found

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Created by
denoland
Publisher
Deno (Official)
Distributed by
Deno
Source
denoland/skills · skills/deno-frontend · commit d5f6ace
Licence
MIT License (redistribution allowed)
  • LICENSE (added by AutoGineer from LICENSE)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by denoland or Deno.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download deno-frontend.zip