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 { GetFunction } from 'camelon';
export type GetInput = { query: { q?: string } };
export type GetOutput = { results: string[] };
export const get: GetFunction<GetInput> = ({ query }) => {
return { results: search(query.q ?? '') };
};
export default function Search({ results }: GetOutput) {
return (
<ul>
{results.map((r) => (
<li safe>{r}</li>
))}
</ul>
);
}Input
GetInput and PostInput type params, query, body, and signals. ajv validates the request against them. A mismatch throws InputValidationError, which becomes a 400. (Resource-only verbs type their input the same way, via PutInput/PatchInput/DeleteInput.)
Output
GetOutput and PostOutput are checked at dev start and on camelon validate. The get 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 PostInput.body type validates both a native form submit and a Datastar submit.
export type PostInput = { 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.
import type { GetFunction } from 'camelon';
export type GetInput = { query: { q?: string } };
export type GetOutput = { hits: number };
export const get: GetFunction<GetInput> = ({ query }) => {
return { hits: (query.q ?? '').length * 3 };
};