mirror of
https://github.com/langgenius/dify.git
synced 2026-05-13 08:57:28 +08:00
4.8 KiB
4.8 KiB
Contract Patterns
Table of Contents
- Intent
- Minimal structure
- Core workflow
- Query usage decision rule
- Mutation usage decision rule
- Thin hook 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 or keep feature hooks only for orchestration.
- Combine multiple queries or mutations.
- Share domain-level derived state or invalidation helpers.
- Prefer
web/features/{domain}/hooks/*for feature-owned workflows.
- Treat
web/service/use-{domain}.tsas legacy.- Do not create new thin service wrappers for oRPC contracts.
- When touching existing wrappers, inline direct
consoleQueryormarketplaceQueryconsumption when the wrapper is only a passthrough.
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.
const createTagMutation = useMutation(consoleQuery.tags.create.mutationOptions())
Thin Hook Decision Rule
Remove thin hooks when they only rename a single oRPC query or mutation helper.
Keep hooks when they orchestrate business behavior across multiple operations, own local workflow state, or normalize a feature-specific API.
Prefer feature vertical hooks for kept orchestration. Do not move new contract-first wrappers into web/service/use-*.
Use:
const deleteTagMutation = useMutation(consoleQuery.tags.delete.mutationOptions())
Keep:
const applyTagBindingsMutation = useApplyTagBindingsMutation()
useApplyTagBindingsMutation is acceptable because it coordinates bind and unbind requests, computes deltas, and exposes a feature-level workflow rather than a single endpoint passthrough.
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. - Do not create business-layer helpers whose only purpose is to call
consoleQuery.xxx.mutationOptions()orqueryOptions(). - Do not introduce new
web/service/use-*files for oRPC contract passthroughs. - 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>