# Typed SDK & client

**Declare a contract action once and everything the caller needs — the typed method, the query key, the cache entry, the TanStack options — is derived from it, in one import.**

The `@netscript/sdk` is the **typed client and data layer** for NetScript: it turns an [`@orpc/contract`](https://rickylabs.github.io/netscript/explanation/contracts/) into a discovered, type-safe service client, wraps each contract action in a **cache-first query factory** (KV-backed SWR), and bridges those into [TanStack Query](https://tanstack.com/query) for islands. The contract object is the single source of truth — the *same* object the [service](https://rickylabs.github.io/netscript/services-sdk/services/) implements is the one the client imports, so caller and server **cannot drift**.

beta

![A contract object flows into a generated typed client, then a cache-first query factory backed by Deno KV, then into a Fresh island; the same contract object is imported by the oRPC service so the two ends cannot drift.](https://rickylabs.github.io/netscript/netscript/assets/diagrams/sdk-data-flow.svg)

*One contract → typed client → KV cache-first query → island. The service imports the same contract, so a renamed field is a compile error on both sides.*

## What it is

The SDK is layered. **L1** is the typed client: `createServiceClient<Contract>()` resolves a service URL from [Aspire service discovery](https://rickylabs.github.io/netscript/explanation/aspire/) and returns a callable object whose method signatures are inferred from the contract. **L2** wraps each contract action in a query factory (`createQueryFactories`) that runs through a shared KV-backed cache provider and exposes `queryOptions`/`mutationOptions`/`clientKey`/`key`/`getCachedEntry` per action. **L3** is the `defineServices()` preset that builds clients, server query factories, and frontend query utils from one contract map. You consume the layer you need and can drop down without rewiring. See [Contracts](https://rickylabs.github.io/netscript/explanation/contracts/) for the type-flow theory.

> Use this when
>
> Reach for the SDK when an app or a service needs to
>
> call a typed oRPC service
>
> and render its data with
>
> cache-first, server-hydrated queries
>
> . Use the bare
>
> `createServiceClient`
>
> for service-to-service RPC; add
>
> createQueryFactories
>
> when you want KV-backed SWR and TanStack option helpers; reach for
>
> Fresh
>
> when an island must hydrate that same query key on the client.

## Declare once, derived everywhere

The classic data-layer tax is the [hand-rolled fetch wrapper](https://rickylabs.github.io/netscript/web-layer/query/): a `lib/api.ts` full of `fetch()` calls whose request and response types are re-typed by hand from whatever the server currently returns. Every server change costs a second edit in the wrapper, a third in the query keys, and whichever one is forgotten becomes a runtime bug. For an AI agent working the codebase the tax is worse — each of those copies is a separate turn, and a stale one is a wrong answer the agent cannot see until runtime.

The SDK collapses those copies into one declaration. tRPC made this move famous for TypeScript APIs — declare a procedure on the server and the client's types follow, "end-to-end typesafe." NetScript keeps that declare-once property and shifts *where* the declaration lives: not inside server code, but in a standalone, versioned contract package that the server, the client, **and** the OpenAPI/Scalar surface all import. And the derivation goes further than method types — from one contract action the factory derives the server KV cache key (`key()`), the client TanStack key (`clientKey()`), the `queryOptions`/`mutationOptions` helpers, and the SWR cache reads. One edit to the contract updates the client, the docs surface, and the query layer together — there is no separate turn to keep them in sync.

This is how a production chat application built on NetScript wires its dashboard: the typed client is built straight off the contract type, so a route added to the contract shows up on the client already typed. The mechanics of each layer are below; the type-flow theory is in [Contracts](https://rickylabs.github.io/netscript/explanation/contracts/).

## Learn → / Do →

[Learn — Build a service + client  Track D walks contract → typed client → query factory → definePage layer → island, end to end.](https://rickylabs.github.io/netscript/netscript/tutorials/storefront/02-catalog-service/)

[Do — Discover a service  Task recipe: resolve a service URL from Aspire env and stand up a typed client for it.](https://rickylabs.github.io/netscript/netscript/services-sdk/how-to/discover-services/)

## Minimal example

A single `lib/api-clients.ts` is the spine of the data layer: build the typed client from a contract, then derive a query factory from `{ contract, client }`. Every consumer — server loader or island — imports from here.

```ts
// apps/playground/lib/api-clients.ts
import { createServiceClient } from '@netscript/sdk/client';
import { createQueryFactories } from '@netscript/sdk/query';
import { ordersContract } from '@contracts';

// L1 — typed client. `serviceName` resolves a URL via Aspire discovery.
export const ordersClient = createServiceClient<typeof ordersContract>({
  contract: ordersContract,
  serviceName: 'orders',
});

// L2 — cache-first query factory bound to that client (KV-backed SWR).
export const api = createQueryFactories({
  orders: { contract: ordersContract, client: ordersClient },
});

// Direct typed call: `.list()` is fully inferred from the contract.
const recent = await ordersClient.list({ limit: 10 });
```

> Plugin services add a routerName
>
> A first-party plugin API (workers, sagas, triggers) mounts its router under a named segment, so its client needs
>
> routerName
>
> in addition to
>
> serviceName
>
> :
>
> createServiceClient<typeof workersContract>({ contract, serviceName: 'workers-api', routerName: 'workers' })
>
> . Plain workspace services omit it.

## Key types first

The query factory is the heart of the data layer. Each contract action becomes an **`ActionMethod`** carrying the full cache-first surface — keys, prefetch, cached reads, and TanStack option helpers. These are the methods you call from a `definePage` loader and an island; reading them first explains the rest of the page.

**ActionMethod — per-action query helpers (api.orders.list.*)**

| Name | Type | Description |
| --- | --- | --- |
| `queryOptions(props, options?)` | `(input, ActionQueryOptions?) => QueryOptions` | TanStack queryOptions with a typed queryKey and queryFn derived from the contract. The primary helper a loader/island passes to useQuery. |
| `mutationOptions(options?)` | `(ActionMutationOptions?) => MutationOptions` | TanStack mutationOptions with a typed mutationKey and mutationFn for writes (e.g. updateStatus). |
| `key(props)` | `(input) => readonly [string, action, string]` | Canonical SERVER cache key [resource, action, serializedInput] used by the KV cache. |
| `clientKey(props?)` | `(input?) => readonly unknown[]` | CLIENT-side TanStack query key, prefix-matchable for invalidateQueries. clientKey() vs key() is the server↔client split. |
| `getCachedEntry(props)` | `(input) => Promise \| null>` | Read cached data WITH its cache timestamp (cachedAt) — the SWR primitive a layer loader uses to decide stale-reload. |
| `getCachedData(props)` | `(input) => Promise` | Read cached data only, without fetching. |
| `prefetch(props, options?)` | `(input, QueryParams?) => void` | Warm the cache in the background (fire-and-forget). |
| `invalidate()` | `() => Promise` | Invalidate all cached queries for this action. |

## Constructing the client & factory

The two L1/L2 constructors and their options. `createServiceClient` is the only call that touches discovery; `createQueryFactories` is pure wiring over `{ contract, client }`.

**createServiceClient(options) — CreateServiceClientOptions**

| Name | Type | Description |
| --- | --- | --- |
| `contract` | `TContract — required` | The @orpc/contract object. Drives both client typing AND HTTP method inference. |
| `serviceName` | `string — required` | Service name registered in Aspire / NetScript config. Resolved to a URL via discovery (services__{name}__{protocol}__{index}; browser uses the VITE_ mirror). |
| `routerName` | `string?` | Router-name segment for URL path construction. Required for plugin API services, omitted for plain services. |
| `protocol` | `'http' \| 'https'?` | Resolved protocol for service discovery. |
| `apiPath / apiVersion` | `string?` | Base RPC path and API version segment overrides. |
| `propagateTraceContext` | `boolean?` | Auto-propagate W3C traceparent/tracestate headers on each call. |

The L3 alternative builds all three layers from one map: `defineServices({ orders: { contract, serviceName: 'orders' } })` returns `{ clients, queryFactories, queryUtils }` — the same L2 values, just composed in one call.

## Service discovery & query client

`serviceName` is resolved at call time from Aspire-injected environment variables. The `@netscript/sdk/discovery` subpath exposes the resolvers directly when you need a raw URL, and `@netscript/sdk/query-client` provides the browser-side TanStack glue.

**Discovery & query-client helpers**

| Name | Type | Description |
| --- | --- | --- |
| `getServiceUrl(name, protocol, index)` | `@netscript/sdk/discovery` | Resolve a service URL from Aspire browser-VITE or server services__* env. This is how serviceName resolves. |
| `getServiceInfo(name) / isServiceAvailable(name) / getAllServices()` | `@netscript/sdk/discovery` | Inspect a service's endpoints, check availability, or list all server-side Aspire service names (topology). |
| `createNetScriptQueryClient(options)` | `@netscript/sdk/query-client` | A TanStack QueryClient with server-first defaults: staleTime 30s, gcTime 300s, refetchOnWindowFocus false, retry 1. |
| `createServiceQueryUtils()` | `@netscript/sdk/query-client` | Bridge a typed SDK client into oRPC/TanStack frontend query utilities. |
| `bridgeInvalidation(resource, action?)` | `@netscript/sdk/query-client` | Build a client-side invalidation filter ({ queryKey }) for queryClient.invalidateQueries(). |
| `toClientKeyPrefix(resource, action?)` | `@netscript/sdk/query-client` | Map a server resource/action to a prefix-matchable client query key, e.g. ['orders','list']. |
| `cacheQuery.setCachedData(key, data, ttl)` | `@netscript/sdk/cache` | Server-only: fire-and-forget pre-warm of an entity into the KV cache. Importing /cache auto-registers the shared provider. |

```ts
// routes/(dashboard)/orders/(_loaders)/orders-list.ts
import { api } from '@/lib/api-clients.ts';

// Cache-first: read the KV entry (with cachedAt) for SWR; fall back to a fetch.
export const loadOrders = async () => {
  const entry = await api.orders.list.getCachedEntry({ limit: 20 });
  if (entry) return entry; // serve cached; SDK reloads stale in the background
  return { data: await api.orders.list.queryOptions({ limit: 20 }).queryFn(), cachedAt: Date.now() };
};
```

```tsx
// orders/(_islands)/OrdersQueryIsland.tsx
import { useQuery, useQueryClient } from '@netscript/fresh/query';
import { api } from '@/lib/api-clients.ts';

// Same contract action → same query key as the server loader, so the island
// hydrates from server state instead of refetching on mount.
const OrdersList = () => {
  const qc = useQueryClient();
  const orders = useQuery(api.orders.list.queryOptions({ limit: 20 }));
  // invalidate by prefix after a write:
  const refresh = () => qc.invalidateQueries({ queryKey: api.orders.list.clientKey() });
  return null; // render orders.data
};
```

```ts
// services/orders/src/routers/v1.ts — service-to-service call
import { safe, isDefinedError } from '@netscript/sdk/client';
import { usersClient } from '@/lib/api-clients.ts';

const [error, user, isDefined] = await safe(usersClient.getById({ id }));
if (error) {
  // narrow to a typed, contract-declared error
  if (isDefinedError(error)) return { code: error.code, status: error.status };
  throw error;
}
```

## Production notes

> /cache and /discovery are server-only
>
> @netscript/sdk/cache
>
> touches Deno KV and
>
> @netscript/sdk/discovery
>
> reads
>
> services__*
>
> env on the server — never import them into island/browser code. Browsers resolve service URLs through the
>
> `VITE_`-mirrored
>
> keys that Aspire injects, and the query factory's cache reads happen on the server loader; the island receives hydrated state and a
>
> clientKey()
>
> for invalidation only.

> clientKey() vs key() — don't cross the streams
>
> key(props)
>
> is the
>
> server
>
> KV cache key (
>
> [resource, action, serializedInput]
>
> );
>
> clientKey(props?)
>
> is the
>
> client
>
> TanStack key (prefix-matchable). Invalidate on the client with
>
> clientKey()
>
> /
>
> toClientKeyPrefix(...)
>
> — passing a server
>
> key()
>
> to
>
> invalidateQueries
>
> will silently match nothing. NetScript is in beta; subpath barrels are stable but signatures may still move.

## Reference →

This hub is intentionally thin — the full generated API (client, query, query-client, cache, collections, discovery, streams, telemetry, ports) lives in the reference.

[sdk](https://rickylabs.github.io/netscript/netscript/reference/sdk/)

[Look up — @netscript/sdk reference  Generated API for every subpath: createServiceClient, createQueryFactories, defineServices, discovery, and the cache engine.](https://rickylabs.github.io/netscript/netscript/reference/sdk/)

[Understand — Contracts  The oRPC contract → implement → handler → typed client → query → island type flow that makes drift a compile error.](https://rickylabs.github.io/netscript/netscript/explanation/contracts/)

[Build — Services & contracts  The server side of the contract: defineService, the fluent builder, OpenAPI/Scalar, and the auth seam.](https://rickylabs.github.io/netscript/netscript/services-sdk/services/)

[Render — Fresh meta-framework  How a definePage layer + island consume these query factories with cache-first hydration.](https://rickylabs.github.io/netscript/netscript/web-layer/)

[Fresh meta-framework](https://rickylabs.github.io/netscript/netscript/web-layer/) [Polyglot tasks](https://rickylabs.github.io/netscript/netscript/background-processing/polyglot-tasks/)
