Typed contracts
A route declares its data shapes as exported types. One declaration drives the typed args, runtime validation, and the OpenAPI spec. You never annotate params, query, or body inline — they come off the input type.
import type { RouteArgs } from 'camelon';
export type LoaderInput = { query: { q?: string } };
export type LoaderOutput = { results: string[] };
export function loader({ query }: RouteArgs) {
return { results: search(query.q ?? '') };
}
export default function Search({ results }: LoaderOutput) {
return (
<ul>
{results.map((r) => (
<li safe>{r}</li>
))}
</ul>
);
}Input
LoaderInput and ActionInput type params, query, body, and signals. ajv validates the request against them. A mismatch throws InputValidationError, which becomes a 400.
Output
LoaderOutput and ActionOutput are checked at dev start and on camelon validate. The loader return, the default parameter, and the head parameter must all be assignable to the output type.
One type for forms and Datastar
A Datastar @post sends the page's signals as the JSON body. A single ActionInput.body type validates both a native form submit and a Datastar submit.
export type ActionInput = { body: { email: string; password: string } };
Run camelon openapi to write the OpenAPI spec and contract artifacts. Run camelon validate to check the whole tree without importing routes.