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.

Concepts

Endpoint

An Endpoint is a declarative, typed description of a single API route. It captures the HTTP method, the URL path (with optional {placeholder} segments), a shelving/schema for the request payload, and a schema for the response. Think of it the way Collection describes a database table — a shared definition both client and server reference.

Factory functions (GET(), POST(), PUT(), PATCH(), DELETE(), HEAD()) create endpoints concisely:

ts
import { GET, POST } from "shelving/api"
import { STRING, NUMBER } from "shelving/schema"

const getUser = GET("/users/{id}", { id: STRING }, { name: STRING, age: NUMBER })
const createPost = POST("/posts", { title: STRING, body: STRING }, { id: STRING })

For GET and HEAD requests, payload fields that don't fill a {placeholder} are appended as ?query params. All other methods send a JSON body.

Providers

An APIProvider is the abstract interface for executing calls against endpoints. ClientAPIProvider is the concrete implementation that uses the global fetch API. Wrap it with ThroughAPIProvider subclasses to layer in behaviour without rewriting transport logic:

ProviderPurpose
ClientAPIProviderConcrete base — sends requests over the network with fetch()
ThroughAPIProviderPass-through base for wrapping another provider
ValidationAPIProviderValidates payload and result against endpoint schemas
DebugAPIProviderLogs fetch attempts, results, and errors to the console
JSONAPIProviderForces JSON request bodies and JSON response parsing
MockAPIProviderRecords calls without sending network requests
MockEndpointAPIProviderDispatches calls through handler objects (useful for unit tests)
XMLAPIProviderForces XML request bodies and returns raw text responses

Extend ThroughAPIProvider to add custom behaviour such as auth headers:

ts
import { ThroughAPIProvider } from "shelving/api"

class AuthAPIProvider<P, R> extends ThroughAPIProvider<P, R> {
  constructor(source: APIProvider<P, R>, readonly token: string) { super(source) }
  override fetch(request: Request): Promise<Response> {
    return super.fetch(new Request(request, {
      headers: { ...Object.fromEntries(request.headers), Authorization: `Bearer ${this.token}` },
    }))
  }
}

Caching

APICache manages EndpointCache objects, one per endpoint. Each EndpointCache manages EndpointStore objects, one per unique payload — keyed by the rendered URL. For GET/HEAD requests, query params are part of the key so ?role=admin and ?role=editor are stored separately. EndpointStore fetches automatically on first read and de-duplicates in-flight requests.

The cache is primarily useful as the backbone of the React integration. Use it directly only when you need a reactive layer outside React.

Usage

Define endpoints and fetch directly

ts
import { GET, POST, ClientAPIProvider, ValidationAPIProvider } from "shelving/api"
import { DATA, STRING } from "shelving/schema"

const UserSchema = DATA({ id: STRING, name: STRING, email: STRING })
const getUser    = GET("/users/{id}", DATA({ id: STRING }), UserSchema)
const createUser = POST("/users", DATA({ name: STRING, email: STRING }), UserSchema)

const provider = new ValidationAPIProvider(new ClientAPIProvider({ url: "https://api.example.com" }))

const user    = await provider.call(getUser, { id: "u_123" })
const created = await provider.call(createUser, { name: "Alice", email: "[email protected]" })

Server-side routing

ts
import { handleEndpoints } from "shelving/api"

const handlers = [
  getUser.handler(({ id }) => db.users.get(id)),
  createUser.handler(({ name, email }) => db.users.create({ name, email })),
]

// In a Cloudflare Worker or similar:
export default {
  fetch(request: Request) {
    return handleEndpoints("https://api.example.com", handlers, request)
  },
}

Testing with MockEndpointAPIProvider

Wires your handler array directly into a mock transport so you can test client and server code together without a real network:

ts
import { MockEndpointAPIProvider } from "shelving/api"

const api = new MockEndpointAPIProvider(handlers, undefined)
const user = await api.call(getUser, { id: "u_123" })

React integration

The shelving/react module's createAPIContext() is the primary way to use a provider in a React app. It creates a context backed by an APICache and exposes a typed APIContext.useAPI() hook that returns reactive EndpointStore instances and suspends automatically while loading.

ts
import { createAPIContext } from "shelving/react"
import { ClientAPIProvider, ValidationAPIProvider } from "shelving/api"

const provider = new ValidationAPIProvider(new ClientAPIProvider({ url: "https://api.example.com" }))
export const { APIContext, useAPI } = createAPIContext(provider)

See the shelving/react module for full usage.

Functions

Go

HEAD()function

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>
Go

GET()function

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>
Go

POST()function

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>
Go

PUT()function

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>
Go

PATCH()function

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>
Go

DELETE()function

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>
Go

handleEndpoints()function

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>

Classes

Go

ClientAPIProviderclass

A client-side API provider that sends requests over the network using fetch().

new ClientAPIProvider<P, R>({ url, options = {}, timeout = 20_000 }: ClientAPIProviderOptions)
Go

MockEndpointAPIProviderclass

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>)
Go

ValidationAPIProviderclass

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>()
Go

CachedAPIProviderclass

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)
Go

APIProviderclass

Abstract base for API providers that send requests to a set of Endpoint definitions rooted at a common base URL.

new APIProvider<P, R>()
Go

MockAPIProviderclass

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" }))
Go

DebugAPIProviderclass

Provider that logs every request, response, and error to the console in detail to help diagnose issues in development.

new DebugAPIProvider<P, R>()
Go

JSONAPIProviderclass

Client API provider that always sends request bodies as JSON and parses responses as JSON.

new JSONAPIProvider<P, R>()
Go

LoggingAPIProviderclass

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)
Go

XMLAPIProviderclass

Client API provider that always sends request bodies as XML and parses responses as plain text.

new XMLAPIProvider<P, R>()
Go

Endpointclass

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>)
Go

EndpointStoreclass

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>)
Go

EndpointCacheclass

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>)
Go

APICacheclass

Cache of EndpointCache objects keyed by Endpoint, providing memoised API results across many endpoints.

new APICache<P, R>(provider: APIProvider<P, R>)

Interfaces

Go

ClientAPIProviderOptionsinterface

Options for constructing a ClientAPIProvider.

{
	readonly url: PossibleURL;
	readonly options?: Omit<RequestOptions, "signal">;
	readonly timeout?: number | undefined;
}
Go

EndpointHandlerinterface

A typed endpoint definition paired with its implementation callback.

{
	readonly endpoint: Endpoint<P, R>;
	readonly callback: EndpointCallback<P, R, C>;
}

Types

Go

MockAPIFetchCalltype

Record of a single mocked fetch, pairing the request with the response the handler returned.

{
	readonly request: Request;
	readonly response: Response;
}
Go

MockAPIRequestCalltype

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;
}
Go

MockAPIResponseCalltype

Record of a single response parse, capturing the endpoint, response, and parsed result.

{
	readonly endpoint: AnyEndpoint;
	readonly response: Response;
	readonly result: unknown;
}
Go

AnyEndpointtype

An Endpoint with any payload and result type, for use where the specific types don't matter.

Endpoint<any, any>
Go

Endpointstype

An immutable list of endpoints.

ImmutableArray<AnyEndpoint>
Go

PayloadTypetype

Extract the payload type from an Endpoint.

X extends Endpoint<infer Y, unknown> ? Y : never
Go

EndpointTypetype

Extract the result type from an Endpoint.

X extends Endpoint<unknown, infer Y> ? Y : never
Go

EndpointCallbacktype

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>
Go

AnyEndpointHandlertype

An EndpointHandler with any payload and result type, for use where the specific types don't matter.

EndpointHandler<any, any, C>
Go

EndpointHandlerstype

A collection of endpoint handlers that can be matched and invoked by handleEndpoints().

Iterable<AnyEndpointHandler<C>>