React Native
Glotto generates a first-class React-Native SDK — not a re-skin of the web client. It
shares the same GlottoIR as the TypeScript target but emits an independent, Hermes-safe,
Metro-friendly package.
Quickstart
npm install @your-org/petstore
import { Client } from '@your-org/petstore';
// the token comes from your secure store rather than a build-time env var
const client = new Client({ token });
// a single call
const pet = await client.pets.createPet({ name: 'Rex' });
// pagination is an async iterator, same surface as the TypeScript SDK
for await (const pet of client.pets.listPets()) {
console.log(pet.name);
}
Secure token storage
The bearer token can be backed by a secure-storage adapter instead of memory:
expo-secure-store(Expo)- Keychain (
react-native-keychain) - MMKV (
react-native-mmkv)
You inject the adapter; the SDK reads/writes the token through it.
Resilient retries on mobile
The retry policy becomes network-aware when you inject the optional deps:
- NetInfo (
@react-native-community/netinfo) — pause retries while offline, resume on reconnect. - AppState — pause retry backoff while the app is backgrounded, resume on foreground. The same injected
AppStatealso pauses long-livedwaitForpolling and SSE/NDJSON streaming while backgrounded (see below), so nothing does work off-screen.
Both are optional: no NetInfo/AppState, no hard dependency.
Streaming & hooks
-
Streaming uses an optional
ReadableStreampolyfill exported from the/streamssubpath, plus NDJSON via async generators. A frame whose payload isn’t valid JSON surfaces a parse error to your loop (never a raw string mistyped as your model); declare terminal sentinels likedata: [DONE]viastreaming.on_event. See Streaming. -
Backgrounding a stream: inject
AppStateon theClient(the same option as the retry pause) and a long-lived SSE/NDJSON stream pauses at the top of its read loop while the app is backgrounded — it stops consuming and yielding events off-screen and resumes on foreground. Callerabortstill tears the stream down immediately. NoAppState, no behavior change. -
Streaming on Expo: React Native’s built-in
fetchbuffers responses, so an SSE/NDJSON call over it fails with an actionable error instead of a silently empty stream. On Expo, opt into streaming with the/exposubpath’s typedexpo/fetchadapter:import { expoStreamingFetch } from '<your-sdk>/expo'; const client = new Client({ fetch: expoStreamingFetch });The
/exposubpath is the only module that imports Expo — bare React Native apps never resolve it (pass any other streaming-capablefetchthe same way). -
TanStack Query 5 hooks are available from the
/hookssubpath for idiomatic data fetching withuseQuery/useInfiniteQuery.
Testing with Jest
The SDK package is ESM-first, so Jest’s default node_modules ignore needs an allowance
for it — with either the react-native or the jest-expo preset:
// jest.config.js
module.exports = {
preset: 'jest-expo', // or 'react-native'
transformIgnorePatterns: [
'node_modules/(?!((jest-)?react-native|@react-native(-community)?|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|<your-sdk>)/)',
],
};
Because every native seam is injected (NetInfo, AppState, secure storage, fetch), unit
tests pass plain objects — no native-module mocks are required to exercise the client.
Unknown response fields
Your API can add a response field without it being a breaking change — but a generated interface gives you no typed way to reach it. The generated models keep it instead: a field the SDK wasn’t generated from is retained on decode, readable through an accessor, and written back out when the model is re-serialized.
const pet = await client.pets.createPet(body);
const extra = petExtraFields(pet);
if ('species' in extra) {
console.log(extra.species);
}
Each model gets its own reader — petExtraFields(pet), tagExtraFields(tag) — typed to that
model, so passing the wrong one is a compile error.
As in the TypeScript SDK, the data was always retained at runtime (the decode is a cast); the readers are what make it reachable and keep it guaranteed rather than incidental.
The retained fields are read-only by design. To send a field your spec doesn’t model yet, use the per-call extra-body escape hatch rather than writing to the retained bag.
Platform integration
-
Hermes-compatible output (no
eval, noFunctionconstructor, noProxy). -
An optional Expo config plugin wires the OAuth deep-link scheme and the iOS keychain entitlement, and can pin the app’s JS engine via its
jsEngineprop. Glotto emits it when the API’s auth capabilities need it. An API without OAuth or bearer-style/secure-storage auth capability—for example, one using only custom or API-key auth—does not receive a./pluginsubpath by default. To opt any React Native target in explicitly, settargets.react_native.expo_plugin: true, then import the public subpath in your Expo config:import withGlotto from '<your-sdk>/plugin'; -
AbortSignal is never polyfilled — the SDK detects and warns if a polyfill is installed.
-
A Metro-friendly package shape: side-effect-free, ESM-first, tree-shakable per resource, with the manifest’s
react-nativefield pointing at shipped TypeScript source so Metro consumes it directly.
Pages, request controls, and files
Paginated methods also expose manual pages, so you can inspect one response and request its successor explicitly. Request overrides apply to the initial call and to each requested next page; see Retries & timeouts for precedence and cancellation.
Binary downloads return an owned byte response with metadata, a bounded read helper, and incremental consumption. Close the response when you stop early. Your generated README demonstrates these operations with your API’s names and the language’s native calling conventions.