Multiple generators
Add TypeScript types and Tanstack Query hooks alongside the Zod validators from [tutorial 01](01-your-first-generation.md), and watch the cross-generator coordination converge.
What you'll build
A project running three stock generators against the same OpenAPI spec, producing:
- Zod validation schemas (from tutorial 01)
- TypeScript type aliases
- Tanstack Query
useQuery/useMutationhooks
The interesting part: the hooks reference the Zod schemas the validator generator produces — and the engine produces each schema exactly once even though both generators want it.
Prerequisites
- The
petstoreproject from tutorial 01. - Deno +
skmtcCLI installed.
Step 1: Install additional generators
skmtc install @skmtc/gen-typescript petstore
skmtc install @skmtc/gen-tanstack-query-fetch-zod petstoreVerify with skmtc list petstore --json — three generators should
be installed now.
Step 2: Inspect the dependency relationships
@skmtc/gen-tanstack-query-fetch-zod doesn't declare a hard
dependency on gen-zod or gen-typescript, but its generated
hooks reference Zod schemas (for validation) and TS types (for
parameters). When all three run together, they converge on shared
output via cross-generator coordination.
You don't need to configure anything for this — coordination is automatic.
Step 3: Regenerate
skmtc generate petstoreThe engine runs all three generators against the same parsed
document. New files appear in src/generated/:
types/pet.generated.ts— now contains bothexport const pet = z.object({...})(from gen-zod) andexport type Pet = {...}(from gen-typescript). One file per schema component, both generators contributing.services/useGetApiPetPetId.generated.ts— the hook file, importingpetandPetfrom the schema file above.
Step 4: Verify the cross-generator coordination
Look inside a hook file:
import {pet} from '@/types/pet.generated.ts'
import {useQuery} from '@tanstack/react-query'
export type UseGetApiPetPetIdArgs = {petId: number};
export const useGetApiPetPetId = ({petId}: UseGetApiPetPetIdArgs) => {
const result = useQuery({
queryKey: ['pet', petId],
queryFn: async () => {
const res = await fetch(`/pet/${petId}`, {
method: 'GET'
})
if (!res.ok) {
const error = await res.text()
throw new Error(error)
}
const data = await res.json()
return pet.parse(data)
}
})
return result
};The pet import is the same pet that gen-zod registered — not a
duplicate. If you run skmtc generate again, the output is
byte-identical: the engine is deterministic.
Try swapping the install order:
skmtc remove petstore @skmtc/gen-zod
skmtc remove petstore @skmtc/gen-typescript
skmtc install @skmtc/gen-typescript petstore
skmtc install @skmtc/gen-zod petstore
skmtc generate petstoreThe output is identical. Generator order doesn't matter — see how idempotency works.
Step 5: Change the schema and watch the fan-out
So far the schema has been a remote URL. Make it local so you can edit it. From the workspace root:
curl -o openapi.json https://petstore3.swagger.io/api/v3/openapi.jsonPoint source at the file in .skmtc/petstore/.settings/client.json
(relative paths resolve against the workspace root):
{ "source": "./openapi.json" }Now add a field. In openapi.json, find
components.schemas.Pet.properties and add:
"nickname": { "type": "string" }Regenerate and look for it:
skmtc generate petstore
grep -rn "nickname" src/generated/Every file that spells out Pet's shape updated in one regenerate —
the type gained a field and the validator gained a rule — while the
hook files that import them stayed consistent without changing. This
is the property you'll lean on daily: edit the schema, regenerate,
and everything derived from it agrees.
Step 6 (optional): break the schema on purpose
While the schema is local, see what a bad item costs. In
openapi.json, change any "$ref" to point at a schema that doesn't
exist — for example "#/components/schemas/DoesNotExist" — and
regenerate:
skmtc generate petstore --json > out.json
jq '.manifest.parseIssues' out.jsonThe run completes. Unaffected files regenerate as normal; the broken
item and everything that depended on it are pruned rather than
mis-generated, and parseIssues names the casualty
(INVALID_DEPENDENCY_REF) with its location. One bad schema never
kills the run — the manifest always tells you exactly what it cost.
Undo the edit and regenerate before moving on.
What just happened
Three generators ran against the same schema, two of them needed the same shared pieces — and each shared piece exists exactly once, referenced by imports. Nothing was deduplicated after the fact: every generator either created a definition or reused one that was already registered, so a duplicate never existed in the first place. The same mechanism is why the rerun was byte-identical and why reordering the generators changed nothing.
How that works — files as keyed maps, insert as create-or-reuse — is one short page: Definitions and files.
Steps 5 and 6 showed the two properties this buys you day to day: a schema edit fans out to every derived artifact in one regenerate, and a schema mistake narrows the output instead of killing the run, with the manifest naming exactly what was skipped.
Next steps
- Tutorial 03: Customize with enrichments —
add per-operation overrides via
client.json - Recipe: Full-stack TypeScript app — add forms, mocks, and tables on top of this stack
- Definitions and files — why this worked: the create-or-reuse mechanism
- Cross-generator coordination concept — the deeper mechanism