dify/.agents/skills/frontend-query-mutation/references/contract-patterns.md

3.5 KiB

Contract Patterns

Table of Contents

  • Intent
  • Minimal structure
  • Core workflow
  • Query usage decision rule
  • Mutation usage decision rule
  • Anti-patterns
  • Contract rules
  • Type export

Intent

  • Keep contract as the single source of truth in web/contract/*.
  • Default query usage to 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

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 the baseline.
    • Add .input(type<...>()) only when the request has params, query, or 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 utilities.
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 to direct *.queryOptions(...) usage at the call site.
  2. If 3 or more call sites share the same extra options, extract a small query helper, not a use-* passthrough hook.
  3. Create web/service/use-{domain}.ts only for orchestration.
    • Combine multiple queries or mutations.
    • Share domain-level derived state or invalidation helpers.
const invoicesBaseQueryOptions = () =>
  consoleQuery.billing.invoices.queryOptions({ retry: false })

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

Mutation Usage Decision Rule

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

Anti-Patterns

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

Contract Rules

  • Input structure: always use { params, query?, body? }.
  • No-input GET: omit .input(...); do not use .input(type<unknown>()).
  • Path params: use {paramName} in the path and match it in the params object.
  • Router nesting: group by API prefix, for example /billing/* becomes billing: {}.
  • No barrel files: import directly from specific files.
  • Types: import from @/types/ and use the type<T>() helper.
  • Mutations: prefer mutationOptions; use explicit mutationKey mainly for defaults, filtering, and devtools.

Type Export

export type ConsoleInputs = InferContractRouterInputs<typeof consoleRouterContract>