shelving/apimodule
Typed, provider-based framework for HTTP API access. Define your routes as Endpoint definitions, then call them through a composable provider stack — the same pattern the shelving/db module uses for databases.
Typed, provider-based framework for HTTP API access. Define your routes as Endpoint definitions, then call them through a composable provider stack — the same pattern the shelving/db module uses for databases.
Options for constructing a ClientAPIProvider.
{
readonly url: PossibleURL;
readonly options?: Omit<RequestOptions, "signal">;
readonly timeout?: number | undefined;
}A client-side API provider that sends requests over the network using fetch().
new ClientAPIProvider<P, R>({ url, options = {}, timeout = 20_000 }: ClientAPIProviderOptions)Provider that mocks an API that calls and matches an array of EndpointHandler objects returned from Endpoint.handler()
new MockEndpointAPIProvider<P, R, C>(handlers: EndpointHandlers<C>, context: C, source?: ClientAPIProvider<P, R>)
Provider that validates payloads and results against the endpoint's schemas, so a source of any type is made type-safe.
new ValidationAPIProvider<P, R>()
API provider wrapper that serves requests through an APICache so repeated calls reuse cached results.
new CachedAPIProvider<P, R>(source: APIProvider<P, R>, maxAge: number = AVOID_REFRESH)
Invalidate the cached result for a specific endpoint and payload.
invalidate(endpoint: Endpoint<PP, RR>, payload: PP): void
Invalidate every cached result for an endpoint, across all payloads.
invalidateAll(endpoint: Endpoint<PP, RR>): void
Refresh the cached result for a specific endpoint and payload.
refresh(endpoint: Endpoint<PP, RR>, payload: PP): void
Refresh every cached result for an endpoint, across all payloads.
refreshAll(endpoint: Endpoint<PP, RR>): void
Abstract base for API providers that send requests to a set of Endpoint definitions rooted at a common base URL.
new APIProvider<P, R>()
Render the full final URL for an API request to a given endpoint with a given payload.
renderURL(endpoint: Endpoint<PP, RR>, payload: PP, caller?: AnyCaller): URL
Create a Request that targets this endpoint with a given base URL.
createRequest(endpoint: Endpoint<PP, RR>, payload: PP, options?: RequestOptions, caller?: AnyCaller): Request
Send a Request and return its Response (defaults to the JavaScript fetch() API).
fetch(request: Request): Promise<Response>
Parse an HTTP Response for this endpoint into a result value.
parseResponse(_endpoint: Endpoint<PP, RR>, response: Response, caller?: AnyCaller): Promise<RR>
Send a payload to an Endpoint and retrieve the parsed result.
call(endpoint: Endpoint<PP, RR>, payload: PP, options?: RequestOptions, caller?: AnyCaller): Promise<RR>
Record of a single mocked fetch, pairing the request with the response the handler returned.
{
readonly request: Request;
readonly response: Response;
}Record of a single request build, capturing the endpoint, payload, options, and built request.
{
readonly endpoint: AnyEndpoint;
readonly payload: unknown;
readonly options: RequestOptions | undefined;
readonly request: Request;
}Record of a single response parse, capturing the endpoint, response, and parsed result.
{
readonly endpoint: AnyEndpoint;
readonly response: Response;
readonly result: unknown;
}Provider that records API calls and serves them from a handler without sending network requests.
new MockAPIProvider<P, R>(handler: RequestHandler = _mockHandler, source: APIProvider<P, R> = new ClientAPIProvider({ url: "https://api.mock.com" }))Provider that logs every request, response, and error to the console in detail to help diagnose issues in development.
new DebugAPIProvider<P, R>()
Client API provider that always sends request bodies as JSON and parses responses as JSON.
new JSONAPIProvider<P, R>()
Provider that logs fetches to the console to keep useful request/response logs in production.
new LoggingAPIProvider<P, R>(source: APIProvider<P, R>, onRequest: Callback<[Request]> = logRequest, onResponse: Callback<[Response, Request]> = logRequestResponse, onError: Callback<[reason: unknown, Request]> = logRequestError)
Provider wrapper that delegates every API operation to a wrapped source provider.
new ThroughAPIProvider<P, R>(source: APIProvider<P, R>)
Client API provider that always sends request bodies as XML and parses responses as plain text.
new XMLAPIProvider<P, R>()
A typed API resource definition pairing a method and path with payload and result schemas.
new Endpoint<P, R>(method: RequestMethod, path: AbsolutePath, payload: Schema<P>, result: Schema<R>)
Render the path for this endpoint with the given payload.
renderPath(payload: P, caller: AnyCaller = this.renderPath): AbsolutePath
Match a method/path pair against this endpoint and return any matched {placeholder} params.
match(method: RequestMethod, path: AbsolutePath, caller: AnyCaller = this.match): RequestParams | undefined
Create an endpoint handler pairing for this endpoint.
handler(callback: EndpointCallback<P, R, C>): EndpointHandler<P, R, C>
Convert this endpoint to a string in METHOD /path form, e.g. GET /user/{id}
toString(): string
An Endpoint with any payload and result type, for use where the specific types don't matter.
Endpoint<any, any>
An immutable list of endpoints.
ImmutableArray<AnyEndpoint>
Extract the payload type from an Endpoint.
X extends Endpoint<infer Y, unknown> ? Y : never
Extract the result type from an Endpoint.
X extends Endpoint<unknown, infer Y> ? Y : never
Define a HEAD endpoint at a path, with validated payload and result types.
HEAD(path: AbsolutePath, payload?: Schema<P>, result?: Schema<R>): Endpoint<P, R>
HEAD(path: AbsolutePath, payload: Schema<P>): Endpoint<P, undefined>
HEAD(path: AbsolutePath, payload: undefined, result: Schema<R>): Endpoint<undefined, R>
Define a GET endpoint at a path, with validated payload and result types.
GET(path: AbsolutePath, payload?: Schema<P>, result?: Schema<R>): Endpoint<P, R>
GET(path: AbsolutePath, payload: Schema<P>): Endpoint<P, undefined>
GET(path: AbsolutePath, payload: undefined, result: Schema<R>): Endpoint<undefined, R>
Define a POST endpoint at a path, with validated payload and result types.
POST(path: AbsolutePath, payload?: Schema<P>, result?: Schema<R>): Endpoint<P, R>
POST(path: AbsolutePath, payload: Schema<P>): Endpoint<P, undefined>
POST(path: AbsolutePath, payload: undefined, result: Schema<R>): Endpoint<undefined, R>
Define a PUT endpoint at a path, with validated payload and result types.
PUT(path: AbsolutePath, payload?: Schema<P>, result?: Schema<R>): Endpoint<P, R>
PUT(path: AbsolutePath, payload: Schema<P>): Endpoint<P, undefined>
PUT(path: AbsolutePath, payload: undefined, result: Schema<R>): Endpoint<undefined, R>
Define a PATCH endpoint at a path, with validated payload and result types.
PATCH(path: AbsolutePath, payload?: Schema<P>, result?: Schema<R>): Endpoint<P, R>
PATCH(path: AbsolutePath, payload: Schema<P>): Endpoint<P, undefined>
PATCH(path: AbsolutePath, payload: undefined, result: Schema<R>): Endpoint<undefined, R>
Define a DELETE endpoint at a path, with validated payload and result types.
DELETE(path: AbsolutePath, payload?: Schema<P>, result?: Schema<R>): Endpoint<P, R>
DELETE(path: AbsolutePath, payload: Schema<P>): Endpoint<P, undefined>
DELETE(path: AbsolutePath, payload: undefined, result: Schema<R>): Endpoint<undefined, R>
A function that handles an endpoint request, receiving a validated payload and returning a result.
(payload: P, request: Request, context: C) => R | Response | Promise<R | Response>
A typed endpoint definition paired with its implementation callback.
{
readonly endpoint: Endpoint<P, R>;
readonly callback: EndpointCallback<P, R, C>;
}An EndpointHandler with any payload and result type, for use where the specific types don't matter.
EndpointHandler<any, any, C>
A collection of endpoint handlers that can be matched and invoked by handleEndpoints().
Iterable<AnyEndpointHandler<C>>
Handle a Request with the first matching endpoint handler after stripping any base-path prefix from the request pathname.
handleEndpoints(base: PossibleURL, handlers: EndpointHandlers<C>, request: Request, context: C, caller?: AnyCaller): Promise<Response>
handleEndpoints(base: PossibleURL, handlers: EndpointHandlers<void>, request: Request, context?: undefined, caller?: AnyCaller): Promise<Response>
Store that loads and tracks the result of calling a single API endpoint with a fixed payload, through an APIProvider.
new EndpointStore<P, R>(endpoint: Endpoint<P, R>, payload: P, provider: APIProvider<P, R>)
Cache of EndpointStore objects for a single endpoint, keyed by the rendered request URL of each payload.
new EndpointCache<P, R>(endpoint: Endpoint<P, R>, provider: APIProvider<P, R>)
Get (or create) the EndpointStore for the given payload.
get(payload: P, caller: AnyCaller = this.get): EndpointStore<P, R>
Fetch (or return a cached result) for the given payload.
call(payload: P, maxAge: number = AVOID_REFRESH, caller: AnyCaller = this.call): Promise<R>
Invalidate a specific store so the next read refetches.
invalidate(payload: P, caller: AnyCaller = this.invalidate): void
Invalidate all stores so the next read of any payload refetches.
invalidateAll(): void
Trigger a refetch on a specific store.
refresh(payload: P, maxAge?: number, caller: AnyCaller = this.invalidate): Promise<void>
Trigger a refetch on all stores.
refreshAll(maxAge?: number): Promise<void>
Cache of EndpointCache objects keyed by Endpoint, providing memoised API results across many endpoints.
new APICache<P, R>(provider: APIProvider<P, R>)
Get (or create) the EndpointCache for the given endpoint.
get(endpoint: Endpoint<PP, RR>): EndpointCache<PP, RR>
get(endpoint: Endpoint): EndpointCache
Fetch (or return a cached result) for the given endpoint and payload.
call(endpoint: Endpoint<PP, RR>, payload: PP, maxAge: number = AVOID_REFRESH, caller: AnyCaller = this.call): Promise<RR>
Invalidate a specific store for an endpoint so the next read refetches.
invalidate(endpoint: Endpoint<PP, RR>, payload: PP): void
Invalidate all stores for an endpoint so the next read of any payload refetches.
invalidateAll(endpoint: Endpoint<PP, RR>): void
Trigger a refetch on a specific store for an endpoint.
refresh(endpoint: Endpoint<PP, RR>, payload: PP, maxAge?: number): void
Trigger a refetch on all stores for an endpoint.
refreshAll(endpoint: Endpoint<PP, RR>, maxAge?: number): void
DBProvider implementation for PostgreSQL using Bun's built-in Bun.sql API. No external database driver is required.
PostgreSQL database provider backed by Bun's built-in Bun.SQL driver.
new BunPostgreSQLProvider<I, T>(sql: SQL)
DBProvider implementations for Cloudflare Workers. Two backends are available: Workers KV for simple key-value storage and D1 for relational SQL.
Minimal interface matching the Cloudflare Workers KV namespace runtime object.
{
get(key: string, options: { type: "json" }): Promise<unknown>;
put(key: string, value: string): Promise<void>;
delete(key: string): Promise<void>;
}get(key: string, options: { type: "json" }): Promise<unknown>put(key: string, value: string): Promise<void>
delete(key: string): Promise<void>
Value that can be passed through the D1 Worker API as a bound parameter.
boolean | null | number | string
Metadata returned by the D1 Worker API alongside a query result.
{
readonly changed_db?: boolean | undefined;
readonly duration?: number | undefined;
readonly last_row_id?: number | undefined;
readonly rows_read?: number | undefined;
readonly rows_written?: number | undefined;
}Result object returned by D1PreparedStatement.run().
{
readonly success: boolean;
readonly meta?: D1Meta | undefined;
readonly results?: readonly T[] | undefined;
}Result object returned by D1Database.exec().
{
readonly count: number;
readonly duration: number;
}Minimal prepared statement interface for D1 databases and sessions.
{
bind(...values: D1Value[]): D1PreparedStatement;
first<T = Record<string, unknown>>(column?: string): Promise<T | null>;
raw<T = unknown[]>(options?: { columnNames?: boolean }): Promise<readonly T[]>;
run<T extends Record<string, unknown> = Record<string, unknown>>(): Promise<D1Result<T>>;
}bind(...values: D1Value[]): D1PreparedStatement
first(column?: string): Promise<T | null>
raw(options?: { columnNames?: boolean }): Promise<readonly T[]>run(): Promise<D1Result<T>>
Minimal D1 binding/session interface used by CloudflareD1Provider.
{
batch<T extends Record<string, unknown> = Record<string, unknown>>(
statements: readonly D1PreparedStatement[],
): Promise<readonly D1Result<T>[]>;
exec?(query: string): Promise<D1ExecResult>;
prepare(query: string): D1PreparedStatement;
}batch(statements: readonly D1PreparedStatement[]): Promise<readonly D1Result<T>[]>
exec(query: string): Promise<D1ExecResult>
prepare(query: string): D1PreparedStatement
SQLite database provider backed by Cloudflare D1.
new CloudflareD1Provider<I, T>(db: D1Database)
Cloudflare Workers KV database provider.
new CloudflareKVProvider<I, T>(kv: KVNamespace)
Typed database provider abstraction. Define your schema once as a Collection, then swap providers (in-memory, SQLite, Firestore, Cloudflare D1, …) without changing any call sites. Providers are composable wrappers — add validation, logging, or caching by stacking them in a chain.
SQL fragment made from template strings plus embedded expressions, ready to be composed into a query.
{
readonly strings: ImmutableArray<string>;
readonly values: ImmutableArray<unknown>;
}Abstract database provider that implements CRUD and query operations by generating and executing SQL.
new SQLProvider<I, T>()
Execute an SQL query built from a template literal and return the resulting rows.
exec(strings: TemplateStringsArray, ...values: ImmutableArray<unknown>): Promise<ImmutableArray<X>>
Define an SQL fragment using Javascript template literal format.
sql(strings: TemplateStringsArray, ...values: ImmutableArray<unknown>): SQLFragment
Define an SQL fragment for an escaped identifier, e.g. "myTable".
sqlIdentifier(name: string): SQLFragment
Define an SQL fragment that extracts a value at a key for comparison, e.g. "a" #>> {"b","c"} in Postgres.
sqlExtract(key: Segments): SQLFragment
Define an SQL fragment that joins a series of fragments with a separator, e.g. "a" = 1 AND "b" = 2.
sqlConcat(values: ImmutableArray<SQLFragment>, separator = ", ", before = "", after = ""): SQLFragment
Define an SQL fragment for setting a list of values, e.g. "a" = 1, "b" = 2.
sqlSetters(data: TT): SQLFragment
Define an SQL fragment for a set of updates, e.g. "a" = 1, "b" = "b" + 5.
sqlUpdates(updates: Updates<TT>): SQLFragment
Define an SQL fragment for a single update action.
sqlUpdate({ action, key, value }: Update): SQLFragmentDefine an SQL fragment for VALUES syntax, e.g. ("a", "b") VALUES (1, 2).
sqlValues(data: Data): SQLFragment
Define an SQL fragment for the WHERE, ORDER BY and LIMIT clauses of a query, e.g. WHERE x = 1 ORDER BY "name" LIMIT 0, 50.
sqlClauses(query: Query<Item>): void
Define an SQL fragment for a WHERE clause, e.g. WHERE x = 1 AND y <= 100.
sqlWhere(query: Query<Item>): void
Define an SQL fragment for a single filter clause on a column, e.g. x = 1 or x IN (1, 2).
sqlFilter({ key, operator, value }: QueryFilter): SQLFragmentDefine an SQL fragment for an ORDER BY clause, e.g. ORDER BY "a" ASC, "b" DESC.
sqlOrder(query: Query<Item>): void
Define an SQL fragment for an individual column in an ORDER BY, e.g. "a" ASC.
sqlSort({ key, direction }: QueryOrder): SQLFragmentDefine an SQL fragment for a LIMIT clause, e.g. LIMIT 50.
sqlLimit(query: Query<Item>): void