React + TanStack + Hono Val Town Project

Full-stack message board built for Val Town.

Tech Stack

Backend

  • Hono web framework
  • Drizzle ORM with SQLite
  • Deno runtime

Frontend

  • React 19
  • TanStack Router (code-first routing)
  • TanStack Query (server state management)
  • Tailwind CSS
  • TypeScript

Features

  • Message board with persistent storage
  • Client-side routing
  • Optimistic updates
  • Server-side data injection
  • Type-safe database operations

Project Structure

├── backend/          # Hono server running on Val Town
│   ├── database/     # Drizzle schema, migrations, and queries
│   └── index.ts      # Main Hono application
├── frontend/         # React app running in browser
│   ├── components/   # React components
│   ├── lib/          # Utilities and hooks
│   └── router.tsx    # TanStack Router configuration
└── shared/           # Code shared between frontend and backend

API Endpoints

  • GET / - Serves the React application with initial data
  • GET /api/messages - Fetch all messages (JSON)
  • POST /api/messages - Create a new message
  • GET /public/** - Static assets (CSS, JS, etc.)
  • /* - All other routes handled by TanStack Router

Val Town Constraints

  • Read-only filesystem after deployment
  • ESM imports via esm.sh for npm packages
  • Code-first routing (no file-based routing)

Implementation

Router

const homeRoute = new Route({ getParentRoute: () => rootRoute, path: "/", component: () => <App {...window.__INITIAL_DATA__} /> });

Query

export function usePostMessage() { return useMutation({ mutationFn: (content: string) => postMessage(content), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["messages"] }); }, }); }

Schema

export const messages = sqliteTable("messages", { id: integer("id").primaryKey({ autoIncrement: true }), content: text("content").notNull(), timestamp: text("timestamp").notNull().default(new Date().toISOString()), });