Type-Safe Routes
Fossyl’s type system is designed to give you compile-time guarantees about your API routes. Every URL parameter, query string, request body, and authentication context is fully typed — no explicit generics required.
URL Parameter Inference
Section titled “URL Parameter Inference”Route paths use :param syntax for dynamic segments. Fossyl extracts these parameters at the type level and exposes them as typed fields on the url object.
export const getUserRoute = {
path: string
method: RestMethod
steps: Steps[]
handler: Function
authenticator?: AuthenticationFunction<any>
validator?: ValidatorFunction<any>
queryValidator?: ValidatorFunction<any>
urlParamValidator?: ValidatorFunction<any>
paginationConfig?: PaginationConfig
hasTransaction: boolean
} = routerRouter<"/api/users"> .createEndpoint<Path extends `/api/users${string}`>(
path: Path,
) => Endpoint<Path, true>("/api/users/:id") .get<Response extends ResponseData>(
handler: (
parameters: { url: { id: string } & { readonly __kind: "url" } } & {
readonly __kind: "parameters"
},
) => () => Promise<Response>,
) => Route((params{ url: { id: string } & { readonly __kind: "url" } } & {
readonly __kind: "parameters"
}) => async () => { const userUserRow = {
id: number
name: string
email: string
created_at: string
} = await userService.getUserRoute = {
path: string
method: RestMethod
steps: Steps[]
handler: Function
authenticator?: AuthenticationFunction<any>
validator?: ValidatorFunction<any>
queryValidator?: ValidatorFunction<any>
urlParamValidator?: ValidatorFunction<any>
paginationConfig?: PaginationConfig
hasTransaction: boolean
}(Number(params{ url: { id: string } & { readonly __kind: "url" } } & {
readonly __kind: "parameters"
}.url{ id: string } & { readonly __kind: "url" }.idstring)); return { typeName: "User" as const, ...userUserRow = {
id: number
name: string
email: string
created_at: string
} }; });The Params<Path> utility type powers this — it parses the path template at the type level and produces an object type like { id: string } or { postId: string; commentId: string }.
Builder Configurations
Section titled “Builder Configurations”Every endpoint starts as a builder. Chain middleware methods — .authenticator(), .validator(), .query(), .paginate() — to configure the handler, then call an HTTP method to produce a Route. The type system exposes only valid transitions at each step.
Open — No Auth, No Body Validation
Section titled “Open — No Auth, No Body Validation”For public GET/DELETE endpoints. The handler receives URL params and optionally parsed query params.
export const getUserRoute = {
path: string
method: RestMethod
steps: Steps[]
handler: Function
authenticator?: AuthenticationFunction<any>
validator?: ValidatorFunction<any>
queryValidator?: ValidatorFunction<any>
urlParamValidator?: ValidatorFunction<any>
paginationConfig?: PaginationConfig
hasTransaction: boolean
} = routerRouter<"/api/users"> .createEndpoint<Path extends `/api/users${string}`>(
path: Path,
) => Endpoint<Path, true>("/api/users/:id") .get<Response extends ResponseData>(
handler: (
parameters: { url: { id: string } & { readonly __kind: "url" } } & {
readonly __kind: "parameters"
},
) => () => Promise<Response>,
) => Route((params{ url: { id: string } & { readonly __kind: "url" } } & {
readonly __kind: "parameters"
}) => async () => { const userUserRow = {
id: number
name: string
email: string
created_at: string
} = await userService.getUserRoute = {
path: string
method: RestMethod
steps: Steps[]
handler: Function
authenticator?: AuthenticationFunction<any>
validator?: ValidatorFunction<any>
queryValidator?: ValidatorFunction<any>
urlParamValidator?: ValidatorFunction<any>
paginationConfig?: PaginationConfig
hasTransaction: boolean
}(Number(params{ url: { id: string } & { readonly __kind: "url" } } & {
readonly __kind: "parameters"
}.url{ id: string } & { readonly __kind: "url" }.idstring)); return { typeName: "User" as const, ...userUserRow = {
id: number
name: string
email: string
created_at: string
} }; });Validated — Body Validation, No Auth
Section titled “Validated — Body Validation, No Auth”For POST/PUT endpoints where anyone can submit data, but the body must be validated.
export const createUserRoute = {
path: string
method: RestMethod
steps: Steps[]
handler: Function
authenticator?: AuthenticationFunction<any>
validator?: ValidatorFunction<any>
queryValidator?: ValidatorFunction<any>
urlParamValidator?: ValidatorFunction<any>
paginationConfig?: PaginationConfig
hasTransaction: boolean
} = routerRouter<"/api/users"> .createEndpoint<Path extends `/api/users${string}`>(
path: Path,
) => Endpoint<Path, true>("/api/users") .authenticator(
headers: Record<string, string>,
) => Promise<{ userId: string } & Authentication>(authenticator(
headers: Record<string, string>,
) => Promise<{ userId: string } & Authentication>) .validator<RequestBody extends unknown>(
validatorFunction: ValidatorFunction<RequestBody>,
) => {
post: <Response extends ResponseData>(
handler: (
auth: { userId: string } & Authentication & { readonly __kind: "auth" },
) => undefined extends RequestBody
? () => Promise<Response>
: (
body: RequestBody & { readonly __kind: "body" },
) => () => Promise<Response>,
) => Route
put: <Response extends ResponseData>(
handler: (
auth: { userId: string } & Authentication & { readonly __kind: "auth" },
) => undefined extends RequestBody
? () => Promise<Response>
: (
body: RequestBody & { readonly __kind: "body" },
) => () => Promise<Response>,
) => Route
}(createUserValidator(
data: unknown,
params?: util.InexactPartial<ParseParams>,
) => { name: string; email: string }) .post<Response extends ResponseData>(
handler: (
auth: { userId: string } & Authentication & { readonly __kind: "auth" },
) => (
body: { name: string; email: string } & { readonly __kind: "body" },
) => () => Promise<Response>,
) => Route((_auth{ userId: string } & Authentication & { readonly __kind: "auth" }) => (body{ name: string; email: string } & { readonly __kind: "body" }) => async () => { const userUserRow = {
id: number
name: string
email: string
created_at: string
} = await userService.createUserRoute = {
path: string
method: RestMethod
steps: Steps[]
handler: Function
authenticator?: AuthenticationFunction<any>
validator?: ValidatorFunction<any>
queryValidator?: ValidatorFunction<any>
urlParamValidator?: ValidatorFunction<any>
paginationConfig?: PaginationConfig
hasTransaction: boolean
}(body{ name: string; email: string } & { readonly __kind: "body" }.namestring, body{ name: string; email: string } & { readonly __kind: "body" }.emailstring); return { typeName: "User" as const, ...userUserRow = {
id: number
name: string
email: string
created_at: string
} }; });The validator function’s return type becomes the body type in the handler — inference flows through automatically.
Authenticated — Auth, No Body Validation
Section titled “Authenticated — Auth, No Body Validation”For GET/DELETE endpoints that require an authenticated user.
export const getTodoRoute = {
path: string
method: RestMethod
steps: Steps[]
handler: Function
authenticator?: AuthenticationFunction<any>
validator?: ValidatorFunction<any>
queryValidator?: ValidatorFunction<any>
urlParamValidator?: ValidatorFunction<any>
paginationConfig?: PaginationConfig
hasTransaction: boolean
} = routerRouter<"/api/todos"> .createEndpoint<Path extends `/api/todos${string}`>(
path: Path,
) => Endpoint<Path, true>("/api/todos/:id") .authenticator(
headers: Record<string, string>,
) => Promise<{ userId: string } & Authentication>(authenticator(
headers: Record<string, string>,
) => Promise<{ userId: string } & Authentication>) .get<Response extends ResponseData>(
handler: (
parameters: {
query: { q: string; limit?: number; offset?: number } & {
readonly __kind: "query"
}
} & { readonly __kind: "parameters" },
) => () => Promise<Response>,
) => Route((paramsRecord<string, string | undefined>) => (_auth{ userId: string } & Authentication & { readonly __kind: "auth" }) => async () => { const todoTodoRow = {
id: number
title: string
completed: number
created_at: string
} = await todoService.getTodoRoute = {
path: string
method: RestMethod
steps: Steps[]
handler: Function
authenticator?: AuthenticationFunction<any>
validator?: ValidatorFunction<any>
queryValidator?: ValidatorFunction<any>
urlParamValidator?: ValidatorFunction<any>
paginationConfig?: PaginationConfig
hasTransaction: boolean
}(Number(paramsRecord<string, string | undefined>.url{ id: string } & { readonly __kind: "url" }.idnumber)); return { typeName: "Todo" as const, ...todoTodoRow = {
id: number
title: string
completed: number
created_at: string
} }; });Full — Auth + Body Validation
Section titled “Full — Auth + Body Validation”For POST/PUT endpoints that need both authentication and a validated body.
export const updateTodoRoute = {
path: string
method: RestMethod
steps: Steps[]
handler: Function
authenticator?: AuthenticationFunction<any>
validator?: ValidatorFunction<any>
queryValidator?: ValidatorFunction<any>
urlParamValidator?: ValidatorFunction<any>
paginationConfig?: PaginationConfig
hasTransaction: boolean
} = routerRouter<"/api/todos"> .createEndpoint<Path extends `/api/todos${string}`>(
path: Path,
) => Endpoint<Path, true>("/api/todos/:id") .authenticator(
headers: Record<string, string>,
) => Promise<{ userId: string } & Authentication>(authenticator(
headers: Record<string, string>,
) => Promise<{ userId: string } & Authentication>) .validator<RequestBody extends unknown>(
validatorFunction: ValidatorFunction<RequestBody>,
) => {
post: <Response extends ResponseData>(
handler: (
auth: { userId: string } & Authentication & { readonly __kind: "auth" },
) => undefined extends RequestBody
? () => Promise<Response>
: (
body: RequestBody & { readonly __kind: "body" },
) => () => Promise<Response>,
) => Route
put: <Response extends ResponseData>(
handler: (
auth: { userId: string } & Authentication & { readonly __kind: "auth" },
) => undefined extends RequestBody
? () => Promise<Response>
: (
body: RequestBody & { readonly __kind: "body" },
) => () => Promise<Response>,
) => Route
}(updateTodoValidator(
data: unknown,
params?: util.InexactPartial<ParseParams>,
) => { title?: string | undefined; completed?: boolean | undefined }) .put<Response extends ResponseData>(
handler: (
parameters: { url: { id: string } & { readonly __kind: "url" } } & {
readonly __kind: "parameters"
},
) => (
auth: { userId: string } & Authentication & { readonly __kind: "auth" },
) => (
body: { title?: string | undefined; completed?: boolean | undefined } & {
readonly __kind: "body"
},
) => () => Promise<Response>,
) => Route((paramsRecord<string, string | undefined>) => (_auth{ userId: string } & Authentication & { readonly __kind: "auth" }) => (body{ title: string } & { readonly __kind: "body" }) => async () => { const todoTodoRow = {
id: number
title: string
completed: number
created_at: string
} = await todoService.updateTodoRoute = {
path: string
method: RestMethod
steps: Steps[]
handler: Function
authenticator?: AuthenticationFunction<any>
validator?: ValidatorFunction<any>
queryValidator?: ValidatorFunction<any>
urlParamValidator?: ValidatorFunction<any>
paginationConfig?: PaginationConfig
hasTransaction: boolean
}(Number(paramsRecord<string, string | undefined>.url{ id: string } & { readonly __kind: "url" }.idnumber), body{ title: string } & { readonly __kind: "body" }); return { typeName: "Todo" as const, ...todoTodoRow = {
id: number
title: string
completed: number
created_at: string
} }; });Paginated — Paginated GET Endpoints
Section titled “Paginated — Paginated GET Endpoints”For collection endpoints that return paginated results. Use .paginate(config) on the endpoint, then chain .get().
export const listTodosRoute = {
path: string
method: RestMethod
steps: Steps[]
handler: Function
authenticator?: AuthenticationFunction<any>
validator?: ValidatorFunction<any>
queryValidator?: ValidatorFunction<any>
urlParamValidator?: ValidatorFunction<any>
paginationConfig?: PaginationConfig
hasTransaction: boolean
} = routerRouter<"/api/todos"> .createEndpoint<Path extends `/api/todos${string}`>(
path: Path,
) => Endpoint<Path, true>("/api/todos") .paginate(
paginationConfig: PaginationConfig,
) => {
validator: <RequestBody extends unknown>(
validatorFunction: ValidatorFunction<RequestBody>,
) => {
post: <Response extends ResponseData>(
handler: (
parameters: {
pagination: PaginationParams & { readonly __kind: "pagination" }
} & { readonly __kind: "parameters" },
) => undefined extends RequestBody
? () => Promise<PaginatedResponse<Response>>
: (
body: RequestBody & { readonly __kind: "body" },
) => () => Promise<PaginatedResponse<Response>>,
) => Route
put: <Response extends ResponseData>(
handler: (
parameters: {
pagination: PaginationParams & { readonly __kind: "pagination" }
} & { readonly __kind: "parameters" },
) => undefined extends RequestBody
? () => Promise<PaginatedResponse<Response>>
: (
body: RequestBody & { readonly __kind: "body" },
) => () => Promise<PaginatedResponse<Response>>,
) => Route
}
authenticator: <Auth extends Authentication>(
authenticationFunction: AuthenticationFunction<Auth>,
) => {
validator: <RequestBody extends unknown>(
validatorFunction: ValidatorFunction<RequestBody>,
) => {
post: <Response extends ResponseData>(
handler: (
parameters: {
pagination: PaginationParams & { readonly __kind: "pagination" }
} & { readonly __kind: "parameters" },
) => undefined extends Auth
? undefined extends RequestBody
? () => Promise<PaginatedResponse<Response>>
: (
body: RequestBody & { readonly __kind: "body" },
) => () => Promise<PaginatedResponse<Response>>
: (
auth: Auth & { readonly __kind: "auth" },
) => undefined extends RequestBody
? () => Promise<PaginatedResponse<Response>>
: (
body: RequestBody & { readonly __kind: "body" },
) => () => Promise<PaginatedResponse<Response>>,
) => Route
put: <Response extends ResponseData>(
handler: (
parameters: {
pagination: PaginationParams & { readonly __kind: "pagination" }
} & { readonly __kind: "parameters" },
) => undefined extends Auth
? undefined extends RequestBody
? () => Promise<PaginatedResponse<Response>>
: (
body: RequestBody & { readonly __kind: "body" },
) => () => Promise<PaginatedResponse<Response>>
: (
auth: Auth & { readonly __kind: "auth" },
) => undefined extends RequestBody
? () => Promise<PaginatedResponse<Response>>
: (
body: RequestBody & { readonly __kind: "body" },
) => () => Promise<PaginatedResponse<Response>>,
) => Route
}
} & {
get: <Response extends ResponseData>(
handler: (
parameters: {
pagination: PaginationParams & { readonly __kind: "pagination" }
} & { readonly __kind: "parameters" },
) => undefined extends Auth
? () => Promise<PaginatedResponse<Response>>
: (
auth: Auth & { readonly __kind: "auth" },
) => () => Promise<PaginatedResponse<Response>>,
) => Route
post: <Response extends ResponseData>(
handler: (
parameters: {
pagination: PaginationParams & { readonly __kind: "pagination" }
} & { readonly __kind: "parameters" },
) => undefined extends Auth
? () => Promise<PaginatedResponse<Response>>
: (
auth: Auth & { readonly __kind: "auth" },
) => () => Promise<PaginatedResponse<Response>>,
) => Route
put: <Response extends ResponseData>(
handler: (
parameters: {
pagination: PaginationParams & { readonly __kind: "pagination" }
} & { readonly __kind: "parameters" },
) => undefined extends Auth
? () => Promise<PaginatedResponse<Response>>
: (
auth: Auth & { readonly __kind: "auth" },
) => () => Promise<PaginatedResponse<Response>>,
) => Route
delete: <Response extends ResponseData>(
handler: (
parameters: {
pagination: PaginationParams & { readonly __kind: "pagination" }
} & { readonly __kind: "parameters" },
) => undefined extends Auth
? () => Promise<PaginatedResponse<Response>>
: (
auth: Auth & { readonly __kind: "auth" },
) => () => Promise<PaginatedResponse<Response>>,
) => Route
}
} & {
get: <Response extends ResponseData>(
handler: (
parameters: {
pagination: PaginationParams & { readonly __kind: "pagination" }
} & { readonly __kind: "parameters" },
) => () => Promise<PaginatedResponse<Response>>,
) => Route
post: <Response extends ResponseData>(
handler: (
parameters: {
pagination: PaginationParams & { readonly __kind: "pagination" }
} & { readonly __kind: "parameters" },
) => () => Promise<PaginatedResponse<Response>>,
) => Route
put: <Response extends ResponseData>(
handler: (
parameters: {
pagination: PaginationParams & { readonly __kind: "pagination" }
} & { readonly __kind: "parameters" },
) => () => Promise<PaginatedResponse<Response>>,
) => Route
delete: <Response extends ResponseData>(
handler: (
parameters: {
pagination: PaginationParams & { readonly __kind: "pagination" }
} & { readonly __kind: "parameters" },
) => () => Promise<PaginatedResponse<Response>>,
) => Route
}({ defaultPageSize: 20, maxPageSize: 100 }) .get<Response extends ResponseData>(
handler: (
parameters: {
query: { q: string; limit?: number; offset?: number } & {
readonly __kind: "query"
}
} & { readonly __kind: "parameters" },
) => () => Promise<Response>,
) => Route((paramsRecord<string, string | undefined>) => async (): Promise<PaginatedResponse<{ typeName"Todo": "Todo"; idnumber: number }>> => { const result{ data: TodoRow[]; hasMore: boolean; total: number } = await todoService.listTodosRoute = {
path: string
method: RestMethod
steps: Steps[]
handler: Function
authenticator?: AuthenticationFunction<any>
validator?: ValidatorFunction<any>
queryValidator?: ValidatorFunction<any>
urlParamValidator?: ValidatorFunction<any>
paginationConfig?: PaginationConfig
hasTransaction: boolean
}(paramsRecord<string, string | undefined>.pagination{ page: number; pageSize: number; hasMore: boolean; total: number }); return { data: result{ data: TodoRow[]; hasMore: boolean; total: number }.data{ typeName: "Todo"; id: number }[].map<U>(
callbackfn: (value: TodoRow, index: number, array: TodoRow[]) => U,
thisArg?: any,
) => U[]((tTodoRow = {
id: number
title: string
completed: number
created_at: string
}) => ({ typeName: "Todo" as const, id: tTodoRow = {
id: number
title: string
completed: number
created_at: string
}.idnumber })), pagination: { page: paramsRecord<string, string | undefined>.pagination{ page: number; pageSize: number; hasMore: boolean; total: number }.pagenumber, pageSize: paramsRecord<string, string | undefined>.pagination{ page: number; pageSize: number; hasMore: boolean; total: number }.pageSizenumber, hasMore: result{ data: TodoRow[]; hasMore: boolean; total: number }.hasMoreboolean, total: result{ data: TodoRow[]; hasMore: boolean; total: number }.totalnumber, }, }; });Paginated routes return PaginatedResponse<T> — a { data: T[], pagination: { page, pageSize, hasMore?, total? } } shape.
All builder configurations produce the same
Routeobject at runtime — it’s a plain{ path, method, steps, handler, ... }struct that adapters consume. The strong dependent typing lives entirely in the builder chain (.createEndpoint().authenticator().validator().get()), not in theRoutetype itself. This means the type ofgetTodo,createUser,listTodos, etc. is all justRoute— the type safety is guaranteed at construction time, not inspection time.
How Inference Works
Section titled “How Inference Works”Fossyl uses a few key techniques to drive type inference without explicit generics:
Builder State Types
Section titled “Builder State Types”Each step in the builder chain returns a distinct type that only exposes valid transitions. TypeScript enforces the FSM at compile time:
- Neither
.authenticator()nor.validator()→ handler receives params only. Available onOpenRouter(all HTTP methods). .authenticator()only → handler receives(params) => (auth) => () => Res. Available onAuthenticatedRouter(all HTTP methods)..validator()only → handler receives(params) => (body) => () => Res. Available onValidatedRouter(POST/PUT only)..authenticator()+.validator()→ handler receives(params) => (auth) => (body) => () => Res. Available onFullRouter(POST/PUT only)..paginate()→ handler receives params with pagination, returnsPaginatedResponse<T>.
QueryableRouter and PaginatedRouter both inherit the OpenRouter interface, so all the above transitions apply to them as well.
Branded Types
Section titled “Branded Types”The Authentication and RequestBody types use unique symbols as brands:
declare const authBrand: unique symbol;type Authentication = { readonly [authBrand]: "Auth" };
declare const requestBrand: unique symbol;type RequestBody = { readonly [requestBrand]: "RequestBody" };authWrapper() applies the Authentication brand at runtime. The brands are what let the type system distinguish between “data from the authenticator” and “data from the validator” — and pass them to the correct handler parameter positions.
Response Data Constraint
Section titled “Response Data Constraint”All route handlers must return objects that include a typeName property:
type ResponseData<TypeName extends string = string> = { typeNameTypeName: TypeName;};This enables self-describing API responses. The ApiResponse wrapper automatically includes the typeName as a type field on the outer envelope:
typeT["typeName"] ApiResponse<T extends ResponseData> = { success"true": "true"; typeT["typeName"]: T["typeName"]; dataT: T;};The Express adapter (and others) wrap all responses in this format automatically.
Error Handling
Section titled “Error Handling”Fossyl leaves error handling to you. Throw from authenticator, validator, or handler and handle it in your adapter’s error middleware. This keeps the type system focused on what matters: correct data flow at compile time.