• Blog
  • Docs
  • Pricing
  • We’re hiring!
Log inSign up
stevekrouse

stevekrouse

remix3-guestbook

Remix 3 Guestbook demo — fetch-router, SSR, client
Public
Like
remix3-guestbook
Home
Code
3
frontend
2
README.md
H
server.ts
Environment variables
Branches
1
Pull requests
Remixes
History
Val Town is a collaborative website to build and scale JavaScript apps.
Deploy APIs, crons, & store data – all from the browser, and deployed in milliseconds.
Sign up now
Code
/
README.md
Code
/
README.md
Search
…
README.md

Remix 3 Guestbook on Val Town

A working demo of Remix 3's fetch-router running on Val Town — no build step, no bundler, just web standards.

Try it live →

What this demonstrates

Server-side (fetch-router):

  • Typed route definitions with route() — nested routes, dynamic params, method constraints
  • Type-safe URL generation: routes.messages.show.href({ id: "42" }) → /messages/42
  • Middleware (logging) via createRouter({ middleware: [...] })
  • router.map(routes, handlers) for matching routes to handler functions
  • Standard Request → Response handlers — works on any runtime (Deno, Node, Bun, Cloudflare)
  • Form handling with request.formData() and POST → 303 redirect pattern

Client-side (@remix-run/dom):

  • Remix 3's custom JSX runtime — no React, no virtual DOM
  • Component model: outer function (setup, runs once) returns inner function (render)
  • State via closure variables + explicit this.update() to re-render
  • Progressive enhancement: the guestbook works fully without JavaScript

Val Town platform:

  • serveFile() from std/utils transpiles .tsx → JS on-the-fly via esm.town
  • @jsxImportSource pragma tells the transpiler which JSX runtime to use
  • Val Town SQLite for persistence
  • Single HTTP entry point, no build configuration

Routes

MethodPatternHandlerDescription
GET/homeSign form + 5 most recent messages
GET/messagesmessages.indexBrowse all messages
POST/messagesmessages.createCreate message, redirect to detail
GET/messages/:idmessages.showSingle message view
GET/frontend/*frontendStatic assets (CSS, transpiled TSX)

How it works

Server: fetch-router

Define your routes as a typed object:

Create val
import { createRouter, route } from "npm:@remix-run/fetch-router@0.16.0"; const routes = route({ home: "/", messages: { index: { method: "GET", pattern: "/messages" }, create: { method: "POST", pattern: "/messages" }, show: { method: "GET", pattern: "/messages/:id" }, }, });

Then map them to handlers. Each handler receives a context with request, url, params, etc. and returns a Response:

Create val
const router = createRouter({ middleware: [(context, next) => { console.log(context.url.pathname); return next(); }], }); router.map(routes, { home() { return new Response("Hello from home"); }, messages: { show({ params }) { return new Response(`Message ${params.id}`); }, create({ request }) { // handle form POST... return Response.redirect("/messages/1", 303); }, }, }); export default (req: Request) => router.fetch(req);

The route objects are also used for URL generation: routes.messages.show.href({ id: "42" }) returns /messages/42. This means links in your HTML are always in sync with your route definitions.

Client: @remix-run/dom

Remix 3 components don't use React. The model is:

Create val
/** @jsxImportSource https://esm.sh/@remix-run/dom@0.4.0 */ import { createRoot } from "https://esm.sh/@remix-run/dom@0.4.0"; import type { Remix } from "https://esm.sh/@remix-run/dom@0.4.0"; function Counter(this: Remix.Handle) { let count = 0; // state is just a variable return () => ( // inner function = render <button on:click={() => { count++; this.update(); }}> Clicked {count} times </button> ); } createRoot(document.getElementById("root")).render(<Counter />);

The outer function runs once (setup). The inner function runs on every render. State lives in closure variables. Call this.update() to trigger a re-render. There's no useState, no hooks rules, no dependency arrays.

Val Town: serving client TSX

Val Town's serveFile() handles the transpilation magic. When a browser requests /frontend/index.tsx:

  1. The route matches /frontend/*
  2. serveFile("/frontend/index.tsx") fetches the file from esm.town with transpilation
  3. The @jsxImportSource pragma tells the transpiler to import jsx/jsxs from @remix-run/dom/jsx-runtime
  4. TypeScript types are stripped, JSX is transformed, and the result is served as text/javascript

No webpack, no vite, no esbuild config. Just a pragma and a function call.

Key differences from Remix 2 / React Router 7

  • No React dependency — @remix-run/dom has its own JSX runtime and component model
  • No bundler — works without a build step on any JS runtime
  • Explicit re-renders — this.update() instead of useState/useEffect
  • fetch-router is just routing — no loaders, no actions (yet), no nested route data loading. You handle Request → Response yourself.
  • Progressive enhancement by default — server renders full HTML pages, client JS is optional enhancement

What's not shown here (yet)

  • <Frame> — Remix 3's HTMX-style component for fetching HTML fragments from the server. Not yet published on npm.
  • @remix-run/events — Composable event handlers (dom.click, pressDown, etc.) for richer interactions.
  • Client-side navigation — Currently uses full page loads between routes. Once <Frame> ships, you'd wrap links in frames for SPA-like transitions.

Remix 3 resources

  • Wake up, Remix — Ryan Florence's announcement
  • Remix 3: Beyond React — Better Stack walkthrough
  • fetch-router on GitHub
  • @remix-run/dom on npm
FeaturesVersion controlCode intelligenceCLIMCP
Use cases
TeamsAI agentsSlackGTM
DocsShowcaseTemplatesNewestTrendingAPI examplesNPM packages
PricingNewsletterBlogAboutCareers
We’re hiring!
Brandhi@val.townStatus
X (Twitter)
Discord community
GitHub discussions
YouTube channel
Bluesky
Open Source Pledge
Terms of usePrivacy policyAbuse contact
© 2026 Val Town, Inc.