API and realtime
oRPC client
apps/web/src/api/orpc.ts:
import type { TamanContract } from '@taman/api-contract';
import type { ContractClient } from '@vinicunca/request/orpc';
import { createRpcClient } from '@vinicunca/request/orpc';
import { createRpcQueryUtils } from '@vinicunca/request/orpc-query';
export const client = createRpcClient<ContractClient<TamanContract>>({
url: `${apiUrl.replace(/\/+$/, '')}/api/rpc`,
credentials: 'include',
});
export const orpc = createRpcQueryUtils(client, { path: ['taman'] });
createRpcClient options (@vinicunca/request/orpc):
https://api.example.com/api/rpc.fetch (tests, SSR, Worker-to-Worker).'include' for cross-origin cookie auth.Re-exported: safe, isDefinedError, ORPCError, LIVE_RETRY.
import { isDefinedError, safe } from '@vinicunca/request/orpc';
const [error, todo] = await safe(client.todo.get({ id }));
if (isDefinedError(error) && error.code === 'NOT_FOUND') {
// typed contract error
}
TanStack Query
import { useMutation, useQuery } from '@tanstack/vue-query';
import { orpc } from '#/api/orpc';
const list = useQuery(orpc.todo.list.queryOptions({
input: { page: 1, pageSize: 20 },
}));
const create = useMutation(orpc.todo.create.mutationOptions({
onSuccess: () => queryClient.invalidateQueries({ queryKey: orpc.todo.list.key() }),
}));
createRpcQueryUtils wraps @orpc/tanstack-query. The path option prefixes every query key (['taman'] here). queryClient is the shared instance in apps/web/src/query-client.ts (also used by the router guard).
Live streams
Contract: todo.live → eventIterator(todoEventSchema). Server: TodoService.subscribe on channel todo:${organizationId}.
Reconnect after drops:
import { LIVE_RETRY } from '@vinicunca/request/orpc';
// pass context: LIVE_RETRY on the live call
LIVE_RETRY sets retry: Infinity and retries unless the error is an ORPCError with status < 500. CORS allows Last-Event-ID. Publisher: Durable Object on Cloudflare (TODO_PUBLISHER), MemoryPublisher in nitro dev (in-process only — multiple Node processes will not see each other's events). Resume retention is 300 seconds.
REST client
Taman's product API is oRPC. @vinicunca/request/http is for other HTTP APIs. The web app's example is dummyjson:
import { createHttpClient } from '@vinicunca/request/http';
import { createHttpQueryUtils } from '@vinicunca/request/http-query';
export const dummyjsonClient = createHttpClient({ baseURL: 'https://dummyjson.com' });
export const dummyjson = createHttpQueryUtils(dummyjsonClient, { key: ['dummyjson'] });
Nothing is sent with cookies unless you pass credentials: 'include'. Install interceptors in this order when you add a real REST backend: envelope → refresh token → error message. See @vinicunca/request.
Contract location
packages/shared/api-contract is the source of truth. Inputs/outputs: TamanInputs, TamanOutputs, TamanContract. Keep the web and API on the same workspace package so types never drift.