camelon

Examples

The interactive demos throughout these docs are live — each one a real camelon route answering its own request. Open the Network tab to watch.

Each app below is a complete, runnable camelon project under examples/ in the repo. Run any of them with pnpm dev in its directory (deno task dev for the Deno one).

counter

The smallest app. A Datastar counter: signals on the client, two resource routes that patch count back over SSE. examples/counter.

export default function Counter() {
  return (
    <div class="d-row" signals='{"count": 0}'>
      <button class="d-btn" on:click="@post('/counter/decrement')"></button>
      <output class="d-num" text="$count">0</output>
      <button class="d-btn" on:click="@post('/counter/increment')">+</button>
    </div>
  );
}
import type { PostFunction } from 'camelon';

export type PostInput = { signals: { count: number } };

export const post: PostFunction<PostInput> = ({ stream, signals }) => {
  stream.patchSignals({ count: signals.count + 1 });
};
import type { PostFunction } from 'camelon';

export type PostInput = { signals: { count: number } };

export const post: PostFunction<PostInput> = ({ stream, signals }) => {
  stream.patchSignals({ count: signals.count - 1 });
};
live

Per-route head export — a route overrides the document <title> and adds its own styles, merged tag-by-tag with the document head. examples/head.

import type { HeadFunction } from 'camelon';

export const head: HeadFunction = () => (
  <>
    <title>Head override — camelon</title>
    <style>{`.d-head{background:linear-gradient(135deg,#6366f1,#a855f7);color:#fff}.d-head p,.d-head code{color:#eef;background:rgba(255,255,255,.15)}`}</style>
  </>
);

export default function Head() {
  return (
    <div class="d-card d-head">
      <strong>This route set its own &lt;title&gt; + background</strong>
      <p>
        via a <code>head</code> export — camelon merged it into the document head. The gradient
        comes from that head's <code>&lt;style&gt;</code>.
      </p>
    </div>
  );
}
live

chat

Streaming SSE chat. The response route appends words to a message bubble with patchElements(html, { selector, mode: 'append' }), then returns the final element. examples/chat.

export default function Chat() {
  return (
    <div class="d-card d-chat">
      <div id="msgs" class="d-msgs"></div>
      <form class="d-row" signals='{"prompt": ""}' on:submit="@get('/chat/response')">
        <input class="d-input" placeholder="Say something…" bind:prompt="" aria-label="message" />
        <button class="d-btn d-wide" type="submit">Send</button>
      </form>
    </div>
  );
}
import type { GetFunction } from 'camelon';

export type GetInput = { signals: { prompt: string } };

async function* reply(prompt: string): AsyncGenerator<string> {
  const text = `you said “${prompt}”. this reply streams back one word at a time.`;
  for (const word of text.split(' ')) {
    await new Promise((r) => setTimeout(r, 70));
    yield word;
  }
}

export const get: GetFunction<GetInput> = async ({ signals, stream }) => {
  const prompt = String(signals.prompt ?? '').trim();
  if (!prompt) return;

  stream.patchElements(await (<p class="d-msg d-me">{prompt}</p>), { selector: '#msgs', mode: 'append' });
  stream.patchSignals({ prompt: '' });

  const id = `r${Date.now()}`;
  stream.patchElements(await (<p class="d-msg d-bot" id={id} />), { selector: '#msgs', mode: 'append' });

  let acc = '';
  for await (const word of reply(prompt)) {
    acc = acc ? `${acc} ${word}` : word;
    stream.patchElements(await (<p class="d-msg d-bot" id={id}>{acc}</p>));
  }
}
live

session

Login, logout, and a guarded dashboard. Cookie sessions, a _middleware.ts auth guard, and one-read flash messages. examples/session.

import { redirect } from 'camelon';
import type { PostFunction } from 'camelon';

export type PostInput = { body: { email: string; password: string } };

export const post: PostFunction<PostInput> = async ({ body, session }) => {
  const user = await authenticate(body);
  session.set('userId', user.id);
  session.flash('toast', 'Welcome back'); // read once on the next page
  throw redirect('/dashboard');
};
import { redirect } from 'camelon';
import type { PostFunction } from 'camelon';

export const post: PostFunction = async ({ session }) => {
  session.destroy(); // clears state, expires the cookie
  throw redirect('/');
};

db

A fruit store with a cart, backed by an injected client through the db arg. examples/db.

import type { GetFunction } from 'camelon';
import type { Fruit, FruitDb } from '../../db';

declare module 'camelon' {
  interface Register {
    db: FruitDb;
  }
}

export type GetOutput = { fruits: Fruit[] };

export const get: GetFunction = ({ db }) => {
  return { fruits: db.fruits() };
};

export default function Db({ fruits }: GetOutput) {
  return (
    <div class="d-card">
      <strong>Fruit store</strong>
      <ul class="d-list">
        {fruits.map((f) => (
          <li>
            {f.emoji} {f.name} — {f.price}¢
          </li>
        ))}
      </ul>
    </div>
  );
}
live

deno-counter

The counter app on Deno — Deno.serve(await createHandler(...)), no Express, no Node. Proof the core is runtime-agnostic. examples/deno-counter.

browser-counter

camelon's web-standard core running in a browser service worker — a portability demo, not a production target. See runtime targets. examples/browser-counter.