Skip to content

API ReferencedefineApi

defineApi

Creates a data source with base URL, options, middleware, and endpoint groups.

4 min readUpdated both modes

Import

ts
import { defineApi } from "@noverax/courier/define"

Signature

ts
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:

ts
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

api/index.ts
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 sources

With Options

api/index.ts
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:

ts
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:

ts
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:

ts
const api = defineApi({
  baseURL: "...",
  defaultQueryParams: { locale: "en", format: "json" },
  groups: { users },
})
// GET /users → GET /users?locale=en&format=json

middleware

Array of middleware functions applied to every request. See Middleware.

bodySerializer

Custom body encoder (e.g., for URL-encoded forms):

ts
const api = defineApi({
  baseURL: "...",
  bodySerializer: (data) =>
    new URLSearchParams(Object.entries(data).map(([k, v]) => [k, String(v)])),
  groups: { users },
})

searchParamsSerializer

Custom search params encoder:

ts
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.

ts
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.

ts
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.