orpc-contract-first

Pass

Guide for implementing oRPC contract-first API patterns in Dify frontend. Trigger when creating or updating contracts in web/contract, wiring router composition, integrating TanStack Query with typed contracts, migrating legacy service calls to oRPC, or deciding whether to call queryOptions directly vs extracting a helper or use-* hook in web/service.

@langgenius
NOASSERTION3/11/2026
63out of 100
(0)
132.2k
1
2

Install Skill

Skills are third-party code from public GitHub repositories. SkillHub scans for known malicious patterns but cannot guarantee safety. Review the source code before installing.

Install globally (user-level):

npx skillhub install langgenius/dify/orpc-contract-first

Install in current project:

npx skillhub install langgenius/dify/orpc-contract-first --project

Suggested path: ~/.claude/skills/orpc-contract-first/

AI Review

Instruction Quality60
Description Precision75
Usefulness51
Technical Soundness70

Scored 63 for precise, specific trigger description and working TypeScript examples that teach a real architectural pattern. Brief content (1765b) limits instruction quality, but the decision rule and query usage guidance add genuine value for Dify frontend contributors.

Review based on previous version

SKILL.md Content

---
name: orpc-contract-first
description: Guide for implementing oRPC contract-first API patterns in Dify frontend. Trigger when creating or updating contracts in web/contract, wiring router composition, integrating TanStack Query with typed contracts, migrating legacy service calls to oRPC, or deciding whether to call queryOptions directly vs extracting a helper or use-* hook in web/service.
---

# oRPC Contract-First Development

## Intent

- Keep contract as single source of truth in `web/contract/*`.
- Default query usage: call-site `useQuery(consoleQuery|marketplaceQuery.xxx.queryOptions(...))` when endpoint behavior maps 1:1 to the contract.
- Keep abstractions minimal and preserve TypeScript inference.

## Minimal Structure

```text
web/contract/
├── base.ts
├── router.ts
├── marketplace.ts
└── console/
    ├── billing.ts
    └── ...other domains
web/service/client.ts
```

## Core Workflow

1. Define contract in `web/contract/console/{domain}.ts` or `web/contract/marketplace.ts`
   - Use `base.route({...}).output(type<...>())` as baseline.
   - Add `.input(type<...>())` only when request has `params/query/body`.
   - For `GET` without input, omit `.input(...)` (do not use `.input(type<unknown>())`).
2. Register contract in `web/contract/router.ts`
   - Import directly from domain files and nest by API prefix.
3. Consume from UI call sites via oRPC query utils.

```typescript
import { useQuery } from '@tanstack/react-query'
import { consoleQuery } from '@/service/client'

const invoiceQuery = useQuery(consoleQuery.billing.invoices.queryOptions({
  staleTime: 5 * 60 * 1000,
  throwOnError: true,
  select: invoice => invoice.url,
}))
```

## Query Usage Decision Rule

1. Default: call site directly uses `*.queryOptions(...)`.
2. If 3+ call sites share the same extra options (for example `retry: false`), extract a small queryOptions helper, not a `use-*` passthrough hook.
3. Create `web/service/use-{domain}.ts` only for orchestration:
   - Combine multiple queries/mutations.
   - Share domain-level derived state or invalidation helpers.

```typescript
const invoicesBaseQueryOptions = () =>
  consoleQuery.billing.invoices.queryOptions({ retry: false })

const invoiceQuery = useQuery({
  ...invoicesBaseQueryOptions(),
  throwOnError: true,
})
```

## Mutation Usage Decision Rule

1. Default: call mutation helpers from `consoleQuery` / `marketplaceQuery`, for example `useMutation(consoleQuery.billing.bindPartnerStack.mutationOptions(...))`.
2. If mutation flow is heavily custom, use oRPC clients as `mutationFn` (for example `consoleClient.xxx` / `marketplaceClient.xxx`), instead of generic handwritten non-oRPC mutation logic.

## Key API Guide (`.key` vs `.queryKey` vs `.mutationKey`)

- `.key(...)`:
  - Use for partial matching operations (recommended for invalidation/refetch/cancel patterns).
  - Example: `queryClient.invalidateQueries({ queryKey: consoleQuery.billing.key() })`
- `.queryKey(...)`:
  - Use for a specific query's full key (exact query identity / direct cache addressing).
- `.mutationKey(...)`:
  - Use for a specific mutation's full key.
  - Typical use cases: mutation defaults registration, mutation-status filtering (`useIsMutating`, `queryClient.isMutating`), or explicit devtools grouping.

## Anti-Patterns

- Do not wrap `useQuery` with `options?: Partial<UseQueryOptions>`.
- Do not split local `queryKey/queryFn` when oRPC `queryOptions` already exists and fits the use case.
- Do not create thin `use-*` passthrough hooks for a single endpoint.
- Reason: these patterns can degrade inference (`data` may become `unknown`, especially around `throwOnError`/`select`) and add unnecessary indirection.

## Contract Rules

- **Input structure**: Always use `{ params, query?, body? }` format
- **No-input GET**: Omit `.input(...)`; do not use `.input(type<unknown>())`
- **Path params**: Use `{paramName}` in path, match in `params` object
- **Router nesting**: Group by API prefix (e.g., `/billing/*` -> `billing: {}`)
- **No barrel files**: Import directly from specific files
- **Types**: Import from `@/types/`, use `type<T>()` helper
- **Mutations**: Prefer `mutationOptions`; use explicit `mutationKey` mainly for defaults/filtering/devtools

## Type Export

```typescript
export type ConsoleInputs = InferContractRouterInputs<typeof consoleRouterContract>
```