Skip to content

API ReferencedefineApi

defineApi

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

3 min readboth 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
  deduplicate?: boolean
  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() }),
  ],

  // Enable request deduplication
  deduplicate: true,

  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 },
})

deduplicate

Cancel a read still in flight when the same endpoint is read again. Writes are never cancelled. See Abort & Dedup.

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.

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. An endpoint keeps them off its calls with forward: false. See User Sessions.