Skip to content
Agentmart
Submit a tool

networking-and-server-state

Builds TanStack Query server state over an ApiClient - validated responses, keys, retries, optimistic writes, offline.

Skill by zakariaf

IndexedMIT★ 11
Write a review

Install

Works with Claude Code, Claude apps, Codex, Cursor, Gemini CLI, GitHub Copilot, VS Code, OpenCode, Goose, Windsurf. Read the repository before you give any tool access to your data.

npx agentmarket install skill/zakariaf-networking-and-server-state

Asks which of your tools to add it to (Claude Code, Codex, Cursor, VS Code, Windsurf, Gemini CLI, Claude Desktop) and shows the change first. See https://agentmart.dev/cli.

Get the weekly diff

The weekly diff: one email a week with new agent tools, notable updates and grade changes, plus security alerts for tools you follow. Double opt-in, one-click unsubscribe.

We use your email only for this newsletter. Privacy

About

Builds TanStack Query server state over an ApiClient - validated responses, keys, retries, optimistic writes, offline. Use when calling an API, adding a query or mutation, or data is stale. Not for local data (persistence-and-migrations).

Use cases: backend, data

No reviews yet. Installed it? Reviews come from verified installers only. How reviews work

Permissions

What this skill can do and what it asks for, from its SKILL.md and code. Checked Oct 26, 2026.

Tools
No tools found
Secrets
None asked for
Install scripts
None found
Runs as
Node.js package

Network hosts it talks to 1

  • codeload.github.com · found in install command

Files it touches

  • ~/.claude/skills/networking-and-server-state

What's inside

Skill name
networking-and-server-state
Description
Builds TanStack Query server state over an ApiClient - validated responses, keys, retries, optimistic writes, offline. Use when calling an API, adding a query or mutation, or data is stale. Not for local data (persistence-and-migrations).
License
MIT
Instructions
211 lines in SKILL.md · view on GitHub

Before installing

  • Ships executable scripts

Files (91)

  • SKILL.md
  • shared.json · assets/
  • api-failure-message.ts · examples/
  • app-providers.tsx · examples/
  • app-root.tsx · examples/
  • create-device-services.test.ts · examples/composition-root/
  • create-device-services.ts · examples/composition-root/
  • create-test-services.ts · examples/composition-root/
  • services-context.tsx · examples/composition-root/
  • environment.ts · examples/
  • root-layout.tsx · examples/
  • task-list-state.ts · examples/
  • task-pages.ts · examples/
  • use-tasks-model.test.tsx · examples/
  • use-tasks-model.ts · examples/
  • api-client-and-transport.md · references/
  • checker-rules.md · references/
  • mutations-and-optimistic-updates.md · references/
  • online-focus-and-offline.md · references/
  • queries-and-cache.md · references/
  • schemas-and-failures.md · references/
  • testing-server-state.md · references/
  • check-lib.mjs · scripts/
  • check-server-state.mjs · scripts/
  • api-rules.mjs · scripts/lib/
  • query-rules.mjs · scripts/lib/
  • server-state-source.mjs · scripts/lib/
  • source-scan.mjs · scripts/lib/
  • selftest.mjs · scripts/
  • api-client.ts · templates/src/api/
  • api-error.test.ts · templates/src/api/
  • api-error.ts · templates/src/api/
  • api-failure.test.ts · templates/src/api/
  • api-failure.ts · templates/src/api/
  • api-query-options.ts · templates/src/api/
  • build-url.test.ts · templates/src/api/
  • build-url.ts · templates/src/api/
  • create-query-client.test.ts · templates/src/api/
  • create-query-client.ts · templates/src/api/
  • fetch-api-client.test.ts · templates/src/api/
  • fetch-api-client.ts · templates/src/api/
  • query-managers.test.ts · templates/src/api/
  • query-managers.ts · templates/src/api/
  • query-provider.test.tsx · templates/src/api/
  • query-provider.tsx · templates/src/api/
  • task-keys.test.ts · templates/src/api/tasks/
  • task-keys.ts · templates/src/api/tasks/
  • task-mutations.test.ts · templates/src/api/tasks/
  • task-mutations.ts · templates/src/api/tasks/
  • task-queries.test.ts · templates/src/api/tasks/
  • task-queries.ts · templates/src/api/tasks/
  • task-schemas.test.ts · templates/src/api/tasks/
  • task-schemas.ts · templates/src/api/tasks/
  • to-api-failure.test.ts · templates/src/api/
  • to-api-failure.ts · templates/src/api/
  • api-base-url.test.ts · templates/src/config/
  • api-base-url.ts · templates/src/config/
  • app-variant.test.ts · templates/src/config/
  • app-variant.ts · templates/src/config/
  • result.test.ts · templates/src/core/
  • …and 31 more

SKILL.md (first lines)

# Networking and server state

The app talks to its server through one typed `ApiClient` port whose only transport calls `fetch` with
a deadline, checks every response against a valibot schema and never rejects; server data lives in
TanStack Query's cache, defined once per resource in `src/api/`, read in model hooks, retried only when
a retry can help, kept fresh by React Native focus and connectivity, and honest about offline. A
zero-dependency checker proves it.

## Rules that must hold

1. **One network seam.** `fetch`, `XMLHttpRequest`, `WebSocket`, `EventSource` and `sendBeacon` appear
   only in `src/api/`, and `fetch` in exactly one file, `src/api/fetch-api-client.ts`; no HTTP client
   package (axios, ky, `expo/fetch` imports). `src/api/api-client.ts` exports `ApiClient`,
   `src/api/api-failure.ts` exports `ApiFailure`. Why: one place owns deadlines, validation, auth
   headers and the privacy inventory, and lint can ban the rest.
2. **The ApiClient never rejects.** `request()` returns `Result<T, ApiFailure>`: offline, timeouts,
   cancellation, HTTP errors and invalid bodies are typed values; every awaited `fetch` and body read
   sits inside one `try/catch`; a failure is classified by who aborted (the caller's signal, the
   client's own deadline) and by status, never by error text. Why: expo/fetch, React Native's fetch and
   Node's fetch word every error differently, and a rejection that escapes crashes a release handler.
3. **Every attempt has a deadline and honours the caller's signal.** The transport aborts each attempt
   after `timeoutMs` (default 15 s, body included), links TanStack's `signal`, and every `queryFn` passes
   `signal` to the request. Why: Android's OkHttp client has no timeouts, so a stalled request never
   ends; TanStack cancels an abandoned fetch only when the `queryFn` read the signal.
4. **Every response passes its schema, tolerantly.** Each request names a valibot `schema`; the
   transport `safeParse`s every 2xx body and records an invalid one once (method, path, issue paths,
   never the body); no `.json() as T`, no `.json()` or `JSON.parse` outside the transport; response
   schemas use `v.object` (never `v.strictObject`) and wrap closed sets in `v.fallback`. Why: a cast
   checks nothing, and a server that adds a field or an enum value must not break installed apps.
5. **The base URL comes from config, over HTTPS.** `EXPO_PUBLIC_API_URL` per EAS environment, read by
   the app's `src/config` reader and checked by `parseApiBaseUrl` (https outside development); no URL
   literal in `src/api/`, no `baseUrl: '<literal>'`, no `NSAllowsArbitraryLoads` or
   `usesCleartextTraffic` switch. Why: each variant talks to its own server, and release builds block
   cleartext on iOS (ATS) and Android 9+, so a global switch ships to the store.
6. **Queries and mutations are defined once, next to their keys.** Each resource has
   `src/api/<resource>/<resource>-keys.ts` (hierarchical, `all` root, `as const`),
   `-schemas.ts`, `-queries.ts` (`queryOptions`/`infiniteQueryOptions`) and `-mutations.ts`
   (`mutationOptions`); no inline key arrays, no `queryFn`/`mutationFn` or `.request({ method, path })`
   elsewhere. Why: one typo in an inline key misses an invalidation, and `queryOptions` ties the key to
   the data type for `getQueryData`/`setQueryData` without casts.
7. **Server state is read in the feature's model hook.** TanStack hooks appear only in
   `src/features/**/use-*.ts(x)`, `src/hooks/` and `src/api/`; views get plain props; query data is
   never copied into a store, `useState` or an effect (the house ESLint config catches the effect copy,
   not this checker). Why: two copies drift on every refetch, and the view stays testable without a
   client.
8. **One QueryClient per app run, with a failure-aware retry policy.** `createQueryClient` in
   `src/api/` is the only `new QueryClient` in app code, called once from AppRoot's `startApp` (a
   `useState` initializer, after the data layer and the error sink; never at module level or in a render body); queries retry only retryable failures
   (no connection, deadline, 408, 429 after `Retry-After`, 5xx), at most 3 times; mutations retry only
   with an `idempotencyKey`; never `retry: true`/`Infinity`/more than 3. Why: TanStack's default spins
   about seven seconds on a 404, and a retried POST without a key can create two tasks.
9. **Focus and online come from React Native, through one port.** `connectQueryManagers` feeds
   `focusManager` from AppState (`active` only) and `onlineManager` from the `ConnectivityPort`, whose
   one expo-network adapter (the only importer of expo-network or NetInfo) is created by the
   composition root; `QueryProvider` mounts the client and the wiring in the root providers. Online
   means `isConnected` alone; Android's `isInternetReachable` is only the `reachability()` hint for
   banners, ads and purchases. Why: React Native has no window events, so without it nothing pauses
   offline, resumes on reconnect or refetches on return; and Android marks some working networks
   unvalidated for good, where pausing on reachability would keep every request waiting forever.
10. **Every write says what it does to the cache; optimistic writes can roll back.** A mutation
    invalidates or sets the keys it changed in `onSuccess`/`onSettled`, or carries
    `// no-cache-effect: <why>`; an `onMutate` that writes the cache cancels in-flight queries first,
    snapshots, restores the snapshot in `onError` and invalidates in `onSettled`. Why: otherwise lists
    show old data, or a refused write stays on screen as if it happened.
11. **Offline pauses, it does not fail.** Keep `networkMode: 'online'` (a local-only query says
    `// network-mode-always: <why>`); show cached data with an offline notice, never an error in its
    place; a persisted cache sets `buster` and `maxAge`; writes that must survive a kill go to the
    outbox (persistence-and-migrations) and sync with their id as `idempotencyKey`. Why: paused work
    resumes by itself, and a restored cache in an old shape would skip every schema.
12. **Tests use the seams, never the network.** The transport gets a fake `FetchLike`; everything above
    it gets `createFakeApiClient()` from `test/api/`, which checks canned bodies through the request's
    schema and rejects unscripted requests; no test replaces the global `fetch` or mocks `expo/fetch`.
    Why: a replaced global leaks across tests and proves a seam the app never uses.
13. **The UI shows failures by kind.** Model hooks read `apiFailureOf(error)` and map each `kind`
    through one `Record<ApiFailureKind, MessageKey | null>` table; never `error.message`; `cancelled` is
    never shown and expected failures are never recorded. Why: a new kind is then a compile error until
    it has a message in every locale.

## Workflow

1. Decide whether the app has a server. An app with no network at all follows the `offline-only`
   profile of privacy-and-store-declarations instead (the checker prints NOT APPLICABLE there). Read
   [references/api-client-and-transport.md](references/api-client-and-transport.md).
2. Install (stack table, verified 2026-10-03): `npx expo install expo-network` and
   `npm install -E @tanstack/react-query@5.104.0 valibot@1.5.0`.
3. Copy `templates/src/api/` (with `tasks/`), `templates/src/config/api-base-url.ts` and its test,
   `templates/src/services/connectivity/`, and `templates/test/api/`. The synced files
   (`src/core/result.ts`, `src/config/app-variant.ts`, `src/services/error-log/*`,
   `test/flush-microtasks.ts`) come from their owner skills: copy them only if the app lacks them.
4. Environment: declare `EXPO_PUBLIC_API_URL` in `src/env.d.ts` and read it in the app's
   `src/config/environment.ts` as `examples/environment.ts` shows. Setting the value in each EAS
   environment (`eas env:set --name EXPO_PUBLIC_API_URL --value <https-url> --environment <env>
   --visibility plaintext`) is the developer's step: stop and ask before running it.
5. Composition root: add `api` and `connectivity` to `Services`, `createDeviceServices` and
   `createTestServices` as in `examples/composition-root/`.
6. Startup: read the checked environment in the root layout's `decideStartup`
   (`examples/root-layout.tsx`), create the client in AppRoot's `startApp` right after the error sink,
   and render `QueryProvider` right after `AppDataProvider` inside `AppProviders`
   (`examples/app-root.tsx`, `examples/app-providers.tsx`; app-startup-and-bootstrap owns the order).
7. Each resource: replace the Tasks keys, schemas, queries and mutations with the app's own, reading
   [references/schemas-and-failures.md](references/schemas-and-failures.md),
   [references/queries-and-cache.md](references/queries-and-cache.md) and
   [references/mutations-and-optimistic-updates.md](references/mutations-and-optimistic-updates.md).
   Paged lists: `examples/task-pages.ts`.
8. Model hooks: read server state as `examples/use-tasks-model.ts` does, turn query state into a view
   state (`examples/task-list-state.ts`) and failures into message keys
   (`examples/api-failure-message.ts`; add the keys to every catalog with i18n-strings-and-catalogs).
9. Offline needs (a persisted cache, an outbox, sign-out clearing): read
   [references/online-focus-and-offline.md](references/online-focus-and-offline.md).
10. Tests: follow [references/testing-server-state.md](references/testing-server-state.md); run
    `npx jest src/api src/config src/services/connectivity test/api --ci` and `npm run typecheck`.
11. Run `node ${CLAUDE_SKILL_DIR}/scripts/check-server-state.mjs .` from the repo root; fix each `FAIL`
    line (rule ids in [references/checker-rules.md](references/checker-rules.md)) and rerun until
    `RESULT: PASS`.

## Definition of done

- [ ] `fetch` appears only in `src/api/fetch-api-client.ts`; no other network API outside `src/api/`; no HTTP client package.
- [ ] `request()` never rejects: fetch and the body read sit in one `try/catch`; each attempt has a deadline linked to the caller's signal; every `queryFn` passes `signal`.

From zakariaf/ReactNative-Skills, MIT.

More from zakariaf/ReactNative-Skills

Signals

Stars
11
Forks
2
Open issues
0
Last commit (this folder)
Oct 17, 2026 (11 days ago)

From GitHub (fetched Oct 24, 2026).

Details

License
MIT
First seen
Oct 24, 2026
Last checked
Oct 24, 2026

More from zakariaf

Cite this page

networking-and-server-state skill. Agent Mart. https://agentmart.dev/skills/zakariaf-networking-and-server-state (last updated 2026-10-24)