# 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 () => (
  <section>
    <h1>Hello, world!</h1>
  </section>
)
```

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 () => (
  <section>
    <h1>Welcome</h1>
    <p>This is server-rendered HTML.</p>
    <a href="/about">About</a>
  </section>
)
```

```tsx
// src/pages/about.tsx
export default () => (
  <section>
    <h1>About</h1>
    <p>This website was made using futuristic internet technologies.</p>
    <a href="/">&lt;= Back</a>
  </section>
)
```

The default layout automatically includes `src/css/main.css` and `src/client/main.ts`, wraps your page content in `<html>`, `<head>`, and `<body><main>...</main></body>`.

### 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 }) => (
  <section>
    <h1>Hello, {req.query('name') ?? 'stranger'}!</h1>
  </section>
)
```

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 (
    <section>
      <h1>Request Info</h1>
      <p>URL: {req.url}</p>
      <p>User-Agent: {ua}</p>
    </section>
  )
}
```

---

## 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 () => (
  <html lang="en">
    <head>
      <title>My SPA</title>
      <meta charset="utf-8" />
      <meta name="viewport" content="width=device-width, initial-scale=1.0" />
      <meta name="color-scheme" content="light dark" />
      <link href="/css/main.css" rel="stylesheet" />
    </head>
    <body>
      <div id="root" />
      <script src="/client/app.js" type="module" />
    </body>
  </html>
)
```

```tsx
// src/client/app.tsx
import { render } from 'hono/jsx/dom'
import { useState } from 'hono/jsx/dom'

function App() {
  const [count, setCount] = useState(0)

  return (
    <>
      <h1>My SPA</h1>
      <p>Count: {count}</p>
      <button onClick={() => setCount(c => c + 1)}>+</button>
      <button onClick={() => setCount(c => c && c - 1)}>-</button>
    </>
  )
}

render(<App />, document.getElementById('root')!)
```

The client-side app uses `hono/jsx/dom` for React-like rendering with hooks (`useState`, `useEffect`, etc.). Hype automatically transpiles and bundles `src/client/app.tsx` when the browser requests `/client/app.js`.

### Cache-busting in SPAs

Use the git hash to bust asset caches:

```tsx
// src/pages/index.tsx
import { $ } from 'bun'

const GIT_HASH = await $`git rev-parse --short HEAD`.text().then(s => s.trim()).catch(() => 'dev')

export default () => (
  <html lang="en">
    <head>
      <link href={`/css/main.css?${GIT_HASH}`} rel="stylesheet" />
    </head>
    <body>
      <div id="root" />
      <script src={`/client/app.js?${GIT_HASH}`} type="module" />
    </body>
  </html>
)
```

### Passing server data to the client

Inject globals via an inline script:

```tsx
<script dangerouslySetInnerHTML={{
  __html: `
    window.GIT_HASH = '${GIT_HASH}';
    window.API_URL = '${process.env.API_URL}';
  `
}} />
```

---

## Routing

Hype supports two routing layers that work together.

### File-based routing

Files in `src/pages/` are automatically mapped to GET routes:

| File | URL |
|------|-----|
| `src/pages/index.tsx` | `/` |
| `src/pages/about.tsx` | `/about` |
| `src/pages/contact.tsx` | `/contact` |

Files prefixed with `_` are private and won't be served (returns 404). This is used for layouts and other internal files.

### Custom routes

Since Hype extends Hono, you can define any route directly:

```ts
// src/server/index.ts
import { Hype } from '@because/hype'

const app = new Hype()

// JSON API
app.get('/api/users/:id', (c) => {
  return c.json({ id: c.req.param('id'), name: 'Chris' })
})

// Form handling
app.post('/api/contact', async (c) => {
  const body = await c.req.parseBody()
  console.log('Message:', body.message)
  return c.redirect('/thanks')
})

// Custom HTML
app.get('/custom', (c) => {
  return c.html('<h1>Custom page</h1>')
})

export default app.defaults
```

Custom routes are defined before the file-based routes, so they take priority.

---

## Layouts

### Default layout

By default, Hype wraps every page in a simple HTML5 layout that includes:

- `<meta charset="utf-8">`
- `<meta name="viewport">` for responsive design
- `<meta name="color-scheme" content="light dark">` for automatic dark mode
- `<link>` to `/css/main.css` (your `src/css/main.css`)
- `<script>` loading `/client/main.ts` (your `src/client/main.ts`)
- Content wrapped in `<body><main>...</main></body>`

Optionally includes the CSS reset and/or Pico CSS based on your config.

### Custom layout

Create `src/pages/_layout.tsx` to replace the default layout:

```tsx
// src/pages/_layout.tsx
import type { FC } from 'hono/jsx'

const Layout: FC = ({ children, title, props }) => (
  <html lang="en">
    <head>
      <title>{title ?? 'My App'}</title>
      <meta charset="utf-8" />
      <meta name="viewport" content="width=device-width, initial-scale=1.0" />

      <link href="/css/main.css" rel="stylesheet" />
      <script src="/client/main.ts" type="module"></script>
    </head>
    <body>
      <nav>
        <a href="/">Home</a>
        <a href="/about">About</a>
      </nav>
      <main>{children}</main>
      <footer>Built with Hype</footer>
    </body>
  </html>
)

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 }) => (
  <section>
    <h1>Home</h1>
  </section>
)
```

### Static JSX export

```tsx
// src/pages/about.tsx
export default (
  <section>
    <h1>About</h1>
  </section>
)
```

### 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 () => (
  <section>
    <h1>Users</h1>
    <ul>
      {users.map(u => <li>{u.name}</li>)}
    </ul>
  </section>
)
```

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 (
    <section>
      <h1>{user.name}</h1>
    </section>
  )
}
```

### 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
<link href="/css/components.css" rel="stylesheet" />
```

### 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
<link href="/css/pico.css" rel="stylesheet" />
```

### 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
<link href="/css/reset.css" rel="stylesheet" />
```

### 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 () => (
  <section>
    <h1>SSE Demo</h1>
    <div id="time" style="font-size: 2em; font-family: monospace;"></div>

    <script src="/client/main.ts" type="module"></script>
  </section>
)
```

```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 () => (
  <section>
    <img src="/img/logo.png" width="200" />
  </section>
)
```

---

## 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 () => (
  <section>
    <h1>Guestbook</h1>
    <form method="POST" action="/api/message">
      <input name="message" placeholder="Leave a message" required />
      <button type="submit">Send</button>
    </form>
  </section>
)
```

### 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(
    <html>
      <body>
        <h1>Dashboard</h1>
        <div id="data"></div>
        <script dangerouslySetInnerHTML={{ __html: `
          fetch('/api/data')
            .then(r => r.json())
            .then(d => document.getElementById('data').textContent = JSON.stringify(d))
        `}} />
      </body>
    </html>
  )
})

export default app.defaults
```

### Using Hono middleware

Any Hono middleware works with Hype:

```ts
import { Hype } from '@because/hype'
import { cors } from 'hono/cors'
import { basicAuth } from 'hono/basic-auth'

const app = new Hype()

// CORS for API routes
app.use('/api/*', cors())

// Basic auth for admin
app.use('/admin/*', basicAuth({
  username: 'admin',
  password: process.env.ADMIN_PASSWORD!,
}))

export default app.defaults
```
