Under heavy development
StitchAPI

Migration notes

Three spec-correct behaviors — lowercase header names, %20 query spaces, and template-narrowed stitch types — that differ from a naive baseline and 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.

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

Header names go on the wire lowercase

You configure apiKey({ header: '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';

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; query?: Record<string, unknown> | undefined; params?: { id: string | number; } | 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; query?: Record<string, unknown> | undefined; params?: 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; query?: Record<string, unknown> | undefined; params?: 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; query?: Record<string, unknown> | undefined; params?: { id: string | number; } | undefined; body?: unknown; variables?: Record<string, unknown> | undefined; signal?: AbortSignal | undefined; onProgress?: ((progress: AdapterProgress) => void) | undefined; } | undefined'. Type '{ headers?: Record<string, string> | undefined; query?: Record<string, unknown> | undefined; params?: 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; query?: Record<string, unknown> | undefined; params?: { id: string | number; } | 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