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}' });await ({ : { : 1 } }); // `params.id` is required and type-checkedThe 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 fieldWhy 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.