camelon

Handlers & data

A route loads data with HTTP-verb handlers. get runs on GET, post runs on POST. Both take the args bag and return data that the page's default export renders.

A page route with a get handler must export GetOutput — the type its get returns and its default renders.

import type { GetFunction } from 'camelon';

export type GetInput = { query: { name?: string } };
export type GetOutput = { greeting: string };

export const get: GetFunction<GetInput> = ({ query }) => {
  return { greeting: `Hello, ${query.name ?? 'world'}` };
};

export default function Data({ greeting }: GetOutput) {
  return (
    <div class="d-card">
      <strong safe>{greeting}</strong>
      <p>Try adding <code>?name=Ada</code> to the URL above.</p>
    </div>
  );
}
live
export type GetInput = { query: { name?: string } };
export type GetOutput = { greeting: string };

export const get: GetFunction<GetInput> = ({ query }) => {
  return { greeting: `Hello, ${query.name ?? 'world'}` };
};

export default function Page({ greeting }: GetOutput) {
  return <h1 safe>{greeting}</h1>;
}

The args bag

Every handler gets the same object (middleware gets a subset — no signals/stream/db/logger). There is no req, res, or cookies. Read through request, write through response, scratch on ctx, manage state through session.

Key What it is
params path params (:id, :splat)
query parsed query string
body parsed request body
signals Datastar state ({} if not a reactive request)
files multipart uploads by field; .data is a Uint8Array
request raw read: .url .headers .method .signal, .json() .text() .formData()
response .status(code), .header(name, val)
ctx middleware → handler scratch
stream server → client SSE channel
session signed cookie store; auto-commits on change
db your injected client, typed from Register['db']
logger request-bound; carries { requestId, method, path }

Return values

A page route always renders HTML. A resource route returns a string for HTML, an object for JSON, or a Response for full control.

export const get: GetFunction = () => {
  return { ok: true, items: [1, 2, 3] }; // → application/json
};
import type { GetFunction } from 'camelon';

export type GetOutput = { ok: boolean; items: string[] };

export const get: GetFunction = (): GetOutput => ({
  ok: true,
  items: ['apple', 'banana', 'cherry'],
});
live

Logging

logger is request-bound. Every line it writes carries the same requestId, method, and path, so one request's logs correlate end-to-end — no manual threading of an id through your call stack.

export const post: PostFunction<PostInput> = ({ body, logger }) => {
  logger.info('order received', { sku: body.sku });
  // → { level: 'info', msg: 'order received', sku: '…', requestId: '…', method: 'POST', path: '/orders' }
};

Next: typed contracts.