mirror of https://github.com/langgenius/dify.git
3.5 KiB
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
- Define contract in
web/contract/console/{domain}.tsorweb/contract/marketplace.ts.- Use
base.route({...}).output(type<...>())as the baseline. - Add
.input(type<...>())only when the request hasparams,query, orbody. - For
GETwithout input, omit.input(...); do not use.input(type<unknown>()).
- Use
- Register contract in
web/contract/router.ts.- Import directly from domain files and nest by API prefix.
- 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
- Default to direct
*.queryOptions(...)usage at the call site. - If 3 or more call sites share the same extra options, extract a small query helper, not a
use-*passthrough hook. - Create
web/service/use-{domain}.tsonly 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
- Default to mutation helpers from
consoleQueryormarketplaceQuery, for exampleuseMutation(consoleQuery.billing.bindPartnerStack.mutationOptions(...)). - If the mutation flow is heavily custom, use oRPC clients as
mutationFn, for exampleconsoleClient.xxxormarketplaceClient.xxx, instead of handwritten non-oRPC mutation logic.
Anti-Patterns
- Do not wrap
useQuerywithoptions?: Partial<UseQueryOptions>. - Do not split local
queryKeyandqueryFnwhen oRPCqueryOptionsalready exists and fits the use case. - Do not create thin
use-*passthrough hooks for a single endpoint. - These patterns can degrade inference, especially around
throwOnErrorandselect, 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 theparamsobject. - Router nesting: group by API prefix, for example
/billing/*becomesbilling: {}. - No barrel files: import directly from specific files.
- Types: import from
@/types/and use thetype<T>()helper. - Mutations: prefer
mutationOptions; use explicitmutationKeymainly for defaults, filtering, and devtools.
Type Export
export type ConsoleInputs = InferContractRouterInputs<typeof consoleRouterContract>