A minimal Hono + Hono JSX starter for Val Town. Server-rendered HTML, no build step, no React.
| Starter | When to pick it |
|---|---|
basic-html-starter | Static HTML pages. Sprinkle of vanilla JS. No server-side rendering. |
hono-jsx-starter (you're here) | Dynamic server-rendered HTML with JSX. API routes. No React, no SPA. |
react-hono-starter | Client-side React SPA with a Hono API backend. |
index.tsx Hono app, Layout, routes
public/
favicon.svg Static asset
counter.ts Tiny client-side script
The whole app fits in one file. As you grow it, the natural next moves are:
- Pull
Layoutinto its own.tsxfile once you have 3+ pages. - Split routes with
app.route("/api", api)if/api/*grows. - Add more files under
public/— they're served by a singleapp.get("/public/*", ...)route.
JSX pragma. Every .tsx file that uses JSX needs this at the top:
/** @jsxImportSource npm:hono@4/jsx */
Server state. The counter on / is let count = 0 at module scope. It
lives in memory on the Val Town server and survives across requests — but resets
whenever the val restarts (deploys, idle timeouts, etc.). For persistent state,
swap it for std/sqlite or
std/blob.
API routes. The Increment button POSTs to /api/increment, which returns
JSON. Mix server-rendered pages and JSON endpoints in the same Hono app:
app.post("/api/increment", (c) => c.json({ count: ++count }));
Static files. serveFile reads the file by path and serves it with the
right content-type. .ts files are transpiled to JS on request by Val Town — so
you can write public/counter.ts and reference it as
<script type="module" src="/public/counter.ts">. (You'll see an
Open in Val Town: ... comment at the top of the served JS — that's the
transpiler.)
app.get("/public/*", (c) => serveFile(c.req.path));
Client-side JS. Drop a .ts file in public/, reference it from your JSX
with a <script type="module">. No bundler, no build step.
View source.
app.get("/source", c => c.redirect(parseVal().links.self.val)) gives you a
tidy link from your live app back to the code on Val Town.
Error handling. app.onError((err) => Promise.reject(err)) unwraps Hono's
Internal Server Error and rethrows the original — so Val Town's logs show the
real stack trace.
This is a Val Town HTTP val — just open it on val.town and hit the endpoint. No
npm install required. Click the Remix button to make it your own.