Import
import { defineApi } from "@noverax/courier/define"Signature
declare function defineApi<G extends OwnGroups>(
options: BaseOptions & { groups: G }
): DefinedApi<G>groups takes any name but raw or with, and the flat endpoints any name but options, raw or with: see Reserved names. A reserved name is a type error and throws ConfigurationError("RESERVED_ENDPOINT_NAME") when the source is defined.
Parameters
BaseOptions
The core configuration for a data source:
type BaseOptions = {
baseURL?: string
responseType?: "json" | "text" | "blob" | "arrayBuffer" | "formData"
responseParser?: (response: Response) => Promise<unknown>
defaultBody?: Record<string, unknown>
defaultQueryParams?: Record<string, unknown>
allowedHeaders?: string[]
bodySerializer?: (data: Record<string, unknown>) => BodyInit
searchParamsSerializer?: SearchParamsSerializer
coalesce?: boolean
cancelPrevious?: readonly string[]
middleware?: Middleware[]
pathOptions?: PathOptions
timeout?: number
headers?: HeadersInit
signal?: AbortSignal
session?: SessionOptions
forward?: ForwardOptions
}Basic Usage
With Groups
import { defineApi } from "@noverax/courier/define"
import { users } from "./users"
import { posts } from "./posts"
const api = defineApi({
baseURL: process.env.API_URL ?? "http://localhost:4000",
groups: { users, posts },
})
export const sources = { api }
export type Sources = typeof sourcesWith Options
import { defineApi } from "@noverax/courier/define"
import { logMiddleware, authMiddleware } from "@noverax/courier/middleware"
const api = defineApi({
baseURL: process.env.API_URL ?? "http://localhost:4000",
// Response parsing (default: "json")
responseType: "json",
// Default data merged into every POST/PUT/PATCH body
defaultBody: { apiKey: process.env.API_KEY },
// Default params merged into every request
defaultQueryParams: { locale: "en" },
// Request timeout in milliseconds
timeout: 10_000,
// Static headers
headers: {
"X-Client": "courier",
Authorization: `Bearer ${process.env.API_SECRET}`,
},
// Middleware chain
middleware: [
logMiddleware(),
authMiddleware({ getToken: () => getAuthToken() }),
],
// A newer search cancels the one still in flight
cancelPrevious: ["users.search"],
groups: { users, posts },
})Options Reference
baseURL
Sets the base URL prefix for all requests from this source.
responseType
Controls response parsing. Supported: "json" (default), "text", "blob", "arrayBuffer", "formData". A 204 status always returns null. A proxy client resolves to the same kind of value as a direct call.
responseParser
Custom response parser that overrides responseType:
const api = defineApi({
baseURL: "...",
responseParser: async (response) => {
const text = await response.text()
return new DOMParser().parseFromString(text, "text/xml")
},
groups: { users },
})defaultBody
Object merged into every POST/PUT/PATCH body:
const api = defineApi({
baseURL: "...",
defaultBody: { apiKey: "...", version: "v2" },
groups: { users },
})
// POST /users body: { apiKey: "...", version: "v2", name: "Alice" }defaultQueryParams
Object merged into every request's search parameters:
const api = defineApi({
baseURL: "...",
defaultQueryParams: { locale: "en", format: "json" },
groups: { users },
})
// GET /users → GET /users?locale=en&format=jsonmiddleware
Array of middleware functions applied to every request. See Middleware.
bodySerializer
Custom body encoder (e.g., for URL-encoded forms):
const api = defineApi({
baseURL: "...",
bodySerializer: (data) =>
new URLSearchParams(Object.entries(data).map(([k, v]) => [k, String(v)])),
groups: { users },
})searchParamsSerializer
Custom search params encoder:
import qs from "qs"
const api = defineApi({
baseURL: "...",
searchParamsSerializer: {
stringify: (obj) => qs.stringify(obj),
parse: (str) => qs.parse(str),
},
groups: { users },
})coalesce
Identical reads in flight together share one request — the same method, address, headers and body, every credential among them — and each call gets a copy of the answer. On by default; false sends every call of the API alone, and a call can say so for itself with .with({ coalesce: false }). Writes are never shared. See Deduplication & Abort.
cancelPrevious
The reads a newer call cancels while they are still in flight, by their paths, such as ["users.search"] — a search as the user types. The older call rejects with an HTTPError of status 499, whatever its variables were. It runs in a browser or a React Native app, where one user is; on a server it has no effect. A path the API does not define, or one that writes, throws ConfigurationError CANCEL_PREVIOUS_INVALID where the API is defined. See Deduplication & Abort.
allowedHeaders
Additional header names the proxy carries for every endpoint of the source, in both directions: forwarded from the browser to the backend, and passed back from the backend's answer to the browser. An endpoint names its own with .headers([...]), which adds to this list rather than replacing it. The lists govern only what the proxy passes along: headers the source, a middleware or an endpoint's own closure sets are written by you and are always sent, and a browser's copy of one of them is dropped rather than sent in its place.
const users = defineGroup({
prefix: "users",
endpoints: {
list: endpoint.get("").headers(["x-total-count", "link"]),
create: endpoint.post("").headers(["location"]),
},
})Cookies are named the same way, one direction at a time: cookie forwards the caller's cookies to the upstream, and set-cookie passes the upstream's back. Neither crosses where it is not named — see Cookies. An API that keeps a session names its cookies there instead, and refuses cookie and set-cookie in these lists with SESSION_HEADER_CONFLICT: they would carry every cookie past it.
The headers that describe the body — Content-Type, Content-Length, Content-Encoding — are never taken from these lists, because the proxy sends a body of its own.
session
How the API keeps the user's session: the cookies it is kept in, by name or by a prefix ending in *; a cookie holding a bearer token; the double-submit token a write repeats; whether the session cookies it sets reach the browser; the other origins whose pages may make writes with it. A call made while a server serves a user carries that user's session from the request being served; a call made in a browser lets the browser send its own cookies.
defineApi({
baseURL: "https://api.example.com",
session: {
cookies: ["sessionid", "logto_*"],
bearer: "access_token",
csrf: { cookie: "csrftoken", header: "x-csrftoken" },
},
endpoints,
})An endpoint takes it with session: false when it answers every user alike, and session: "unguarded" for a write that does no harm when another site sets it off, such as signing out. See User Sessions.
forward
The headers a call made while serving a request carries from it: requestId, trace, clientAddress, userAgent, locale, and further headers by name. requestId and clientAddress also take header names, for an API that logs the request's id or reads the user's address under names of its own. An endpoint keeps them off its calls with forward: false. See User Sessions.
Related
- User Sessions — the session and the forwarded headers, end to end
- defineGroup — defining endpoint groups
- Defining APIs Guide — in-depth usage patterns
- Middleware — middleware reference