Release candidate — 1.0.0-rc.6
StitchAPI

Migration notes

Auth strategies import from stitchapi/auth, plus three spec-correct behaviors — lowercase header names, %20 query spaces, and template-narrowed stitch types — that surprise migrators.

When you port an existing integration onto a stitch, most things just work. A few do not look like they work, because StitchAPI emits bytes (and infers types) that are correct by the relevant spec but differ from the baseline a hand-rolled wrapper or a URLSearchParams snapshot produced. The mismatch is in your fixture, not the call — so you chase a phantom failure.

This page collects the three that bite during a migration: lowercase header names, %20 for spaces in the query string, and the precise type a template-narrowed stitch infers. Each section is the same shape — what you'd expect, what a stitch emits, why it's conformant, and the one-line adjustment to your fixture or field type.

It opens with a fourth item of a different kind — not a conformance surprise but a deliberate pre-GA break: the auth strategies moved to their own import.

The running example is a generic catalog provider at api.example.com — a media server exposing items behind an API key.

Auth strategies import from stitchapi/auth

You upgrade, and every file that authenticates stops compiling:

Module '"stitchapi"' has no exported member 'bearer'.

The five strategy factories (bearer, apiKey, basic, oauth2, cookieSession) and the four secret resolvers (env, optionalEnv, secretsFile, secretFrom) now live on the stitchapi/auth subpath. Split the import in two:

// before
import { , ,  } from 'stitchapi';
// after
import {  } from 'stitchapi';
import { ,  } from 'stitchapi/auth';

Nothing else changes — same factories, same options, same wire behaviour. Only the module they come from.

Why. auth is the one config slot whose values cost real bytes to build: oauth2 carries a token cache and a refresh state machine, cookieSession a login flow and a cookie jar. On the root barrel those rode along in the advertised entry size for everyone, including projects with no auth at all. Behind their own import, "you only pay for the strategy you use" stops depending on your bundler's tree-shaking and becomes a fact about the module graph — it holds for CommonJS and for a naive bundler too. The whole entry drops from ~25 kB to ~23 kB min+gzip; import { stitch } is unchanged.

There is no compatibility re-export. Re-exporting from the root would put the factories back in the barrel and undo the entire change, and StitchAPI is still pre-GA (1.0.0-rc), where the alias obligation does not yet apply. The break is a compile error at the import line, not a runtime surprise.

A custom strategy is unaffected in kind — AuthStrategy, AuthContext and SecurityScheme are still exported from stitchapi (they type StitchConfig.auth), and stitchapi/auth re-exports them so authoring one takes a single import.

Header names go on the wire lowercase

You configure apiKey({ name: 'X-Catalog-Token', ... }) and your fixture asserts the request carried a header literally named X-Catalog-Token.

A stitch lowercases the header name: it sends x-catalog-token. The default x-api-key is already lowercase; a custom name is lowercased too. The same holds for the headers on an oauth2 token request.

import {  } from 'stitchapi';
import { ,  } from 'stitchapi/auth';

const  = ({
    : 'https://api.example.com',
    : '/items/{id}',
    : ({ : 'X-Catalog-Token', : ('CATALOG_TOKEN') }),
});
// On the wire, the request header is `x-catalog-token`, not `X-Catalog-Token`.

Why it's conformant. HTTP header field names are case-insensitive (RFC 9110 §5.1), so a server compares them case-insensitively and accepts x-catalog-token everywhere it would accept X-Catalog-Token. Lowercase is also the canonical on-the-wire form in HTTP/2 and HTTP/3, where field names are required to be lowercase. Emitting lowercase is the deliberate, portable choice.

The adjustment. A fixture that does a case-sensitive string compare on the header name is the thing to fix — assert against the lowercase name, or compare case-insensitively. If you key a header map, lowercase the key before you look it up. Don't try to force the original casing back; there's no option to, by design.

Spaces in the query string are %20, not +

You search for the matrix and your snapshot — captured from new URLSearchParams({ q: 'the matrix' }).toString() or an application/x-www-form-urlencoded baseline — records q=the+matrix.

A stitch encodes query values with encodeURIComponent, so a space becomes %20: it sends q=the%20matrix.

import {  } from 'stitchapi';

const  = ('https://api.example.com/items');

await ({ : { : 'the matrix' } });
// → GET https://api.example.com/items?q=the%20matrix
//   (a URLSearchParams baseline would have written `q=the+matrix`)

Why it's conformant. +-for-space is specific to the application/x-www-form-urlencoded serialization; it is not a generic URI rule. In a generic URI query a literal + is a valid character, and %20 is the percent-encoding of a space under RFC 3986. Every conformant server decodes both %20 and + back to a single space in the query component, so the two forms are interchangeable on the receiving end — encodeURIComponent simply picks the unambiguous one.

The adjustment. Re-record the byte-for-byte snapshot against the %20 form, or normalize before comparing — decode the query (or replace + with %20) so the assertion compares decoded values rather than encoded bytes. The request the server receives is unchanged.

A template-narrowed stitch won't drop into a uniform Stitch field

stitch() reads the RFC 6570 path-template variables off a string-literal path/url and requires the matching params at the call site. That is the feature — getItem() won't typecheck without params.id:

import {  } from 'stitchapi';

const getItem = ({ : '/items/{id}' });
const getItem: Stitch<unknown, {
    headers?: Record<string, string> | undefined;
    query?: Record<string, unknown> | undefined;
    body?: unknown;
    variables?: Record<string, unknown> | undefined;
    signal?: AbortSignal | undefined;
    onProgress?: ((progress: AdapterProgress) => void) | undefined;
    params: {
        id: string | number;
    };
}>
await ({ : { : 1 } }); // `params.id` is required and type-checked

The cost shows up when you collect heterogeneous stitches in one container — a Map<string, Stitch>, a class field typed Stitch, a registry. The narrowed type carries its specific params shape, and that shape is not assignable to the loose, uniform Stitch:

import { type ,  } from 'stitchapi';

const  = ({ : '/items/{id}' });

const  = new <string, >();
.('item', getItem); // the narrowed params shape rejects the uniform field
Argument of type 'Stitch<unknown, { headers?: Record<string, string> | undefined; query?: Record<string, unknown> | undefined; body?: unknown; variables?: Record<string, unknown> | undefined; signal?: AbortSignal | undefined; onProgress?: ((progress: AdapterProgress) => void) | undefined; params: { ...; }; }>' is not assignable to parameter of type 'Stitch<unknown, StitchInput>'. The types returned by 'with(...)' are incompatible between these types. Type 'Stitch<unknown, { headers?: Record<string, string> | undefined; params?: { id: string | number; } | undefined; query?: Record<string, unknown> | undefined; body?: unknown; variables?: Record<string, unknown> | undefined; signal?: AbortSignal | undefined; onProgress?: ((progress: AdapterProgress) => void) | undefined...' is not assignable to type 'Stitch<unknown, { headers?: Record<string, string> | undefined; params?: Record<string, unknown> | undefined; query?: Record<string, unknown> | undefined; body?: unknown; variables?: Record<string, unknown> | undefined; signal?: AbortSignal | undefined; onProgress?: ((progress: AdapterProgress) => void) | undefined;...'. Types of parameters 'input' and 'input' are incompatible. Type '{ headers?: Record<string, string> | undefined; params?: Record<string, unknown> | undefined; query?: Record<string, unknown> | undefined; body?: unknown; variables?: Record<string, unknown> | undefined; signal?: AbortSignal | undefined; onProgress?: ((progress: AdapterProgress) => void) | undefined; } | undefined' is not assignable to type '{ headers?: Record<string, string> | undefined; params?: { id: string | number; } | undefined; query?: Record<string, unknown> | undefined; body?: unknown; variables?: Record<string, unknown> | undefined; signal?: AbortSignal | undefined; onProgress?: ((progress: AdapterProgress) => void) | undefined; } | undefined'. Type '{ headers?: Record<string, string> | undefined; params?: Record<string, unknown> | undefined; query?: Record<string, unknown> | undefined; body?: unknown; variables?: Record<string, unknown> | undefined; signal?: AbortSignal | undefined; onProgress?: ((progress: AdapterProgress) => void) | undefined; }' is not assignable to type '{ headers?: Record<string, string> | undefined; params?: { id: string | number; } | undefined; query?: Record<string, unknown> | undefined; body?: unknown; variables?: Record<string, unknown> | undefined; signal?: AbortSignal | undefined; onProgress?: ((progress: AdapterProgress) => void) | undefined; }'. Types of property 'params' are incompatible. Type 'Record<string, unknown> | undefined' is not assignable to type '{ id: string | number; } | undefined'. Property 'id' is missing in type 'Record<string, unknown>' but required in type '{ id: string | number; }'.

Why it's conformant. This is ordinary TypeScript: the second type parameter of Stitch<TOut, TIn> defaults to the loose call input, and a stitch with a required params slot narrows TIn to a more specific shape. A more specific type is not assignable to the default one — the narrowing that makes the call site safe is exactly what the uniform field rejects.

The adjustment. Pick the trade-off you want:

import { type ,  } from 'stitchapi';

// Heterogeneous collection — opt back into the loose, uniform type with `as Stitch`.
// You give up call-site param checking for the entries stored this way.
const  = new <string, >();
.('item', ({ : '/items/{id}' }) as );

// Single field you want to keep precise — annotate it with the narrowed type
// and you keep the required-params check (path vars stringify to `string | number`).
class  {
    : <unknown, { : { : string | number } }> = ({
        : '/items/{id}',
    });
}

Use as Stitch when you deliberately want a uniform collection of mixed stitches; annotate the field with the precise type when the stitch is singular and you want to keep the call-site guarantee.

See also

On this page