# Guide to Writing Hype Apps Hype is a thin, opinionated wrapper around [Hono](https://hono.dev) for fast prototyping with Bun. It gives you file-based routing, automatic TypeScript transpilation, SSE, and a default HTML5 layout — all without a build step. Since `Hype extends Hono`, every Hono API (`get`, `post`, `use`, `on`, etc.) works out of the box. ## Table of Contents - [Getting Started](#getting-started) - [Project Structure](#project-structure) - [SSR Apps](#ssr-apps) - [SPA Apps](#spa-apps) - [Routing](#routing) - [Layouts](#layouts) - [Pages](#pages) - [Client-Side JavaScript](#client-side-javascript) - [Styling](#styling) - [Server-Sent Events (SSE)](#server-sent-events-sse) - [Custom API Routes](#custom-api-routes) - [Sub-Routers](#sub-routers) - [Static Files](#static-files) - [Configuration](#configuration) - [Environment Variables](#environment-variables) - [Path Aliases](#path-aliases) - [Utility Functions](#utility-functions) - [Recipes](#recipes) --- ## Getting Started ```sh # add hype to your project bun add @because/hype # create your server entry point mkdir -p src/server src/pages src/css src/client pub ``` Add scripts to `package.json`: ```json { "scripts": { "start": "bun run src/server/index.ts", "dev": "bun run --hot src/server/index.ts" } } ``` Add the required `tsconfig.json`: ```json { "compilerOptions": { "lib": ["ESNext", "DOM"], "target": "ESNext", "module": "Preserve", "moduleDetection": "force", "jsx": "react-jsx", "jsxImportSource": "hono/jsx", "allowJs": true, "moduleResolution": "bundler", "allowImportingTsExtensions": true, "verbatimModuleSyntax": true, "noEmit": true, "strict": true, "skipLibCheck": true, "noFallthroughCasesInSwitch": true, "noUncheckedIndexedAccess": true, "noImplicitOverride": true, "baseUrl": ".", "paths": { "$*": ["src/server/*"], "#*": ["src/client/*"], "@*": ["src/shared/*"] } } } ``` Create your server: ```ts // src/server/index.ts import { Hype } from '@because/hype' const app = new Hype() export default app.defaults ``` Create your first page: ```tsx // src/pages/index.tsx export default () => (

Hello, world!

) ``` Run it: ```sh bun dev ``` That's it. You have a server-rendered page at `http://localhost:3000`. --- ## Project Structure ``` . ├── package.json ├── tsconfig.json ├── pub/ # Static files (served as-is at /) │ └── img/ │ └── logo.png # => /img/logo.png ├── src/ │ ├── server/ │ │ └── index.ts # Server entry point │ ├── pages/ │ │ ├── index.tsx # => GET / │ │ ├── about.tsx # => GET /about │ │ └── _layout.tsx # Custom layout (optional) │ ├── client/ │ │ └── main.ts # Client JS (auto-included by default layout) │ ├── shared/ │ │ └── types.ts # Shared between server and client │ └── css/ │ └── main.css # App CSS (auto-included by default layout) ``` - `src/pages/` — SSR pages, one file per route - `src/client/` — Client-side TypeScript, transpiled and bundled on demand - `src/shared/` — Isomorphic code, available to both server and client - `src/css/` — Stylesheets - `src/server/` — Server-only code - `pub/` — Static assets served directly --- ## SSR Apps SSR is the default mode. Pages in `src/pages/` are server-rendered on every request using Hono's JSX engine and wrapped in a layout. ### Minimal SSR app ```ts // src/server/index.ts import { Hype } from '@because/hype' const app = new Hype() export default app.defaults ``` ```tsx // src/pages/index.tsx export default () => (

Welcome

This is server-rendered HTML.

About
) ``` ```tsx // src/pages/about.tsx export default () => (

About

This website was made using futuristic internet technologies.

<= Back
) ``` The default layout automatically includes `src/css/main.css` and `src/client/main.ts`, wraps your page content in ``, ``, and `
...
`. ### Accessing the request Page components receive `c` (the Hono context) and `req` (the Hono request) as props: ```tsx // src/pages/greet.tsx export default ({ req }) => (

Hello, {req.query('name') ?? 'stranger'}!

) ``` Visit `/greet?name=Chris` to see `Hello, Chris!`. For the full Hono context: ```tsx // src/pages/debug.tsx export default ({ c, req }) => { const ua = req.header('user-agent') return (

Request Info

URL: {req.url}

User-Agent: {ua}

) } ``` --- ## SPA Apps For a single-page app with client-side rendering, disable the default layout and provide your own HTML shell: ```ts // src/server/index.ts import { Hype } from '@because/hype' const app = new Hype({ layout: false }) export default app.defaults ``` ```tsx // src/pages/index.tsx export default () => ( My SPA
{children}
) export default Layout ``` The layout receives: - `children` — the rendered page content - `title` — page title (defaults to `'hype'`) - `props` — the `HypeProps` passed to the constructor (useful for conditional CSS) ### No layout Disable the layout entirely for full control (used in SPA mode): ```ts const app = new Hype({ layout: false }) ``` --- ## Pages Pages are `.tsx` files in `src/pages/` that export a default function (or raw JSX). ### Function export (recommended) ```tsx // src/pages/index.tsx export default ({ c, req }) => (

Home

) ``` ### Static JSX export ```tsx // src/pages/about.tsx export default (

About

) ``` ### Async data in pages Since pages render on the server, you can use top-level await: ```tsx // src/pages/users.tsx const users = await fetch('https://api.example.com/users').then(r => r.json()) export default () => (

Users

) ``` Note: top-level data is fetched once at import time and cached. For per-request data, use the `c` context: ```tsx // src/pages/profile.tsx export default async ({ c, req }) => { const userId = req.query('id') const user = await fetch(`https://api.example.com/users/${userId}`).then(r => r.json()) return (

{user.name}

) } ``` ### Private pages Prefix a file with `_` to prevent it from being served: ``` src/pages/_layout.tsx # not a route — used as the layout src/pages/_helpers.tsx # not a route — internal helpers src/pages/index.tsx # GET / ``` --- ## Client-Side JavaScript ### Automatic transpilation TypeScript files in `src/client/` and `src/shared/` are automatically transpiled and bundled by Bun when requested by the browser. The URL maps directly to the file path: | File | URL | |------|-----| | `src/client/main.ts` | `/client/main.ts` | | `src/client/app.tsx` | `/client/app.js` | | `src/shared/utils.ts` | `/shared/utils.ts` | You can request `.ts` or `.js` extensions — Hype resolves `.ts` and `.tsx` files automatically. ### Module imports Client-side files can import from each other using relative paths: ```ts // src/client/main.ts import { initBurger } from './burger' initBurger() ``` ```ts // src/client/burger.ts export function initBurger() { document.addEventListener('click', (ev) => { const el = (ev?.target as HTMLElement).closest('.burger') as HTMLImageElement if (!el) return el.src = '/img/bite.png' }) } ``` Imports are bundled — the full dependency graph is included in the output, so the browser only needs one request. ### The default layout auto-includes `main.ts` When using the default layout, `src/client/main.ts` is automatically loaded as a module. Just create the file and it works. --- ## Styling ### External CSS Put your styles in `src/css/main.css`. The default layout auto-includes it: ```css /* src/css/main.css */ section { max-width: 500px; margin: 0 auto; } ``` You can also serve additional CSS files from `src/css/`: ```html ``` ### Pico CSS Enable the bundled [Pico CSS](https://picocss.com) for classless styling: ```ts const app = new Hype({ pico: true }) ``` Or include it in a custom layout: ```html ``` ### CSS Reset Enable the bundled CSS reset (Josh W. Comeau's reset): ```ts const app = new Hype({ reset: true }) ``` Or include it in a custom layout: ```html ``` ### Combining options ```ts const app = new Hype({ pico: true, reset: true }) ``` --- ## Server-Sent Events (SSE) Hype provides `app.sse()` for streaming data to the browser. ### Server ```ts // src/server/index.ts import { Hype } from '@because/hype' const app = new Hype() // Stream the current time every second app.sse('/api/time', (send) => { send({ time: Date.now() }) const interval = setInterval(() => send({ time: Date.now() }), 1000) return () => clearInterval(interval) // cleanup on disconnect }) export default app.defaults ``` The `send` function: - Automatically `JSON.stringify`s objects; strings are sent as-is - Accepts an optional second argument for named events: `send(data, 'eventName')` The handler receives `(send, c)` where `c` is the Hono context: ```ts app.sse('/api/user-events', (send, c) => { const userId = c.req.query('userId') // subscribe to user-specific events... }) ``` Return a cleanup function to handle client disconnection. ### Client ```tsx // src/pages/sse.tsx export default () => (

SSE Demo

) ``` ```ts // src/client/main.ts const timeEl = document.getElementById('time') const events = new EventSource('/api/time') events.onmessage = (e) => { const data = JSON.parse(e.data) timeEl!.textContent = new Date(data.time).toLocaleTimeString() } events.onerror = () => { timeEl!.textContent = 'Disconnected' } ``` ### Named events ```ts // Server app.sse('/api/feed', (send) => { send({ type: 'user_joined', name: 'Chris' }, 'activity') send({ message: 'Hello everyone!' }, 'chat') }) ``` ```js // Client const events = new EventSource('/api/feed') events.addEventListener('activity', (e) => { console.log('Activity:', JSON.parse(e.data)) }) events.addEventListener('chat', (e) => { console.log('Chat:', JSON.parse(e.data)) }) ``` ### Test with curl ```sh curl -N http://localhost:3000/api/time ``` --- ## Custom API Routes Since Hype extends Hono, use any Hono routing method: ```ts import { Hype } from '@because/hype' const app = new Hype() // GET with params app.get('/api/users/:id', (c) => { return c.json({ id: c.req.param('id') }) }) // POST with body parsing app.post('/api/users', async (c) => { const body = await c.req.json() return c.json({ created: true, name: body.name }, 201) }) // Form submission app.post('/contact', async (c) => { const form = await c.req.parseBody() console.log(form.email, form.message) return c.redirect('/') }) // Delete app.delete('/api/users/:id', (c) => { return c.json({ deleted: true }) }) export default app.defaults ``` ### Redirect back Use `redirectBack()` to redirect to the referrer: ```ts import { Hype, redirectBack } from '@because/hype' const app = new Hype() app.post('/api/like', async (c) => { // ... handle the like return redirectBack(c, '/') // falls back to '/' if no referrer }) ``` --- ## Sub-Routers Use `Hype.router()` to create sub-routers without duplicate middleware: ```ts // src/server/index.ts import { Hype } from '@because/hype' import api from './api' const app = new Hype() app.route('/api', api) export default app.defaults ``` ```ts // src/server/api.ts import { Hype } from '@because/hype' const api = Hype.router() api.get('/users', (c) => c.json([{ id: 1, name: 'Chris' }])) api.get('/users/:id', (c) => c.json({ id: c.req.param('id') })) api.post('/users', async (c) => { const body = await c.req.json() return c.json(body, 201) }) export default api ``` `Hype.router()` creates a Hype instance that skips middleware registration (no duplicate logging, static file serving, etc.). --- ## Static Files Files in `pub/` are served at the root URL: ``` pub/img/logo.png => /img/logo.png pub/favicon.ico => /favicon.ico pub/robots.txt => /robots.txt ``` Use them in pages: ```tsx export default () => (
) ``` --- ## Configuration ### Constructor options ```ts const app = new Hype({ pico: true, // Include Pico CSS (default: false) reset: true, // Include CSS reset (default: false) prettyHTML: true, // Pretty-print HTML in dev (default: true in dev, false in prod) layout: true, // Wrap pages in layout (default: true) logging: true, // HTTP request logging (default: true) ok: true, // Add GET /ok healthcheck (default: false) }) ``` ### `app.defaults` Always export `app.defaults` as the default export. It returns a Bun.serve-compatible config object: ```ts export default app.defaults // => { port, fetch, idleTimeout: 255 } ``` This triggers lazy route registration and auto-port selection. Bun picks it up as the server config when the file is the entry point. --- ## Environment Variables | Variable | Effect | |----------|--------| | `PORT` | Server port (default: `3000`) | | `NODE_ENV` | Set to `production` to disable auto-port, HTML prettification, and enable transpile caching | | `NO_AUTOPORT` | Disable auto-port selection even in dev | In dev mode, if port 3000 is busy, Hype automatically tries 3001, 3002, etc. (up to 100 attempts). --- ## Path Aliases The recommended `tsconfig.json` defines three path aliases: | Alias | Maps to | Use for | |-------|---------|---------| | `$*` | `src/server/*` | Server-only imports | | `#*` | `src/client/*` | Client-only imports | | `@*` | `src/shared/*` | Isomorphic imports | ```ts // In server code: import { db } from '$db' // => src/server/db.ts // In client code: import { format } from '@utils' // => src/shared/utils.ts ``` --- ## Utility Functions Hype exports a collection of helpers from `@because/hype` (or `@because/hype/utils`): ### Randomness ```ts import { rand, randRange, randItem, randIndex, shuffle, weightedRand, randomId } from '@because/hype' rand() // 1 or 2 (coin flip) rand(6) // 1-6 (roll a die) rand(20) // 1-20 (d20) randRange(5, 10) // 5-10 inclusive randItem(['a', 'b', 'c']) // random element randIndex(['a', 'b', 'c']) // random index (0, 1, or 2) shuffle([1, 2, 3, 4, 5]) // shuffled copy weightedRand() // 1-10, lower numbers more likely randomId() // e.g. "k7x2m1" ``` ### Arrays ```ts import { times, unique } from '@because/hype' times(5) // [1, 2, 3, 4, 5] unique([1, 1, 2, 2, 3, 3]) // [1, 2, 3] ``` ### Colors ```ts import { lightenColor, darkenColor } from '@because/hype' lightenColor('#3498db', 0.5) // blend halfway to white darkenColor('#3498db', 0.5) // blend halfway to black ``` ### Strings ```ts import { capitalize } from '@because/hype' capitalize('hello') // "Hello" ``` ### Dark mode detection (client-side) ```ts import { isDarkMode } from '@because/hype' if (isDarkMode()) { // user prefers dark mode } ``` ### Transpilation ```ts import { transpile } from '@because/hype' const js = await transpile('./src/client/app.tsx') // => bundled ESM JavaScript string ``` --- ## Recipes ### Form with POST handler ```ts // src/server/index.ts import { Hype, redirectBack } from '@because/hype' const app = new Hype() const messages: string[] = [] app.post('/api/message', async (c) => { const { message } = await c.req.parseBody() if (typeof message === 'string') messages.push(message) return redirectBack(c) }) app.get('/api/messages', (c) => c.json(messages)) export default app.defaults ``` ```tsx // src/pages/index.tsx export default () => (

Guestbook

) ``` ### Healthcheck endpoint ```ts const app = new Hype({ ok: true }) // GET /ok => "ok" (200) ``` ### SSR with Pico CSS ```ts const app = new Hype({ pico: true, reset: true }) ``` All your pages instantly get clean, classless styling. Just write semantic HTML. ### Multiple SSE channels ```ts const app = new Hype() // Chat messages app.sse('/api/chat', (send, c) => { const room = c.req.query('room') ?? 'general' // subscribe to chat room... return () => { /* unsubscribe */ } }) // Live notifications app.sse('/api/notifications', (send, c) => { send({ type: 'connected' }) const check = setInterval(async () => { // check for new notifications... send({ count: 5 }, 'notification') }, 5000) return () => clearInterval(check) }) ``` ### Mixing SSR pages with a JSON API ```ts import { Hype } from '@because/hype' const app = new Hype({ pico: true }) // API routes (JSON) app.get('/api/data', (c) => c.json({ items: [1, 2, 3] })) // SSR pages still work via src/pages/ // Custom SSR route: app.get('/dashboard', (c) => { return c.html(

Dashboard