Design system across many APIs
Architectural pattern for sharing a customized generator set across multiple APIs (multiple projects, multiple repos, or multiple teams).
What you'll build
A set of cloned, customized generators that encode your team's house style (naming conventions, output paths, component imports), shared across multiple SKMTC projects. New APIs adopt the design system by referencing the shared clones — no per-project re-customization.
Stack
- 2+ SKMTC projects (in the same workspace, multiple workspaces, or multiple repos)
- One source of truth for the cloned generator code
- The shared clones cover everything that varies "by team house style"; per-API enrichments cover everything that varies "by API"
Setup
Pick a location for the shared clones. Three common patterns:
- Within one workspace. Clone once at workspace root, used by all projects in that workspace via local imports.
- Across workspaces. Publish the customized generators to
a private JSR scope (e.g.,
@yourorg/gen-zod) and install into each workspace. - As a git submodule. Add the customized generator source as a git submodule in each consuming workspace.
This recipe uses pattern 1 (single workspace) for concreteness; patterns 2 and 3 are mentioned in variations.
Step-by-step
Clone the foundational generators
# In the workspace root
skmtc clone shared \
-g @skmtc/gen-typescript \
-g @skmtc/gen-zod \
-g @skmtc/gen-shadcn-formThis creates a .skmtc/shared/ project with three cloned
generators. The "shared" project isn't a real consumer — it's a
holding area for the customized source.
Encode house style in toIdentifierName and toExportPath
Open each cloned generator's src/base.ts and apply your house
style. The naming and path functions are config fields on the
toTsModelProjectionBase({...}) call — not free-standing
exports:
// .skmtc/shared/gen-zod/src/base.ts
import { capitalize } from '@skmtc/core'
import { toTsModelProjectionBase } from '@skmtc/lang-typescript'
import type { TsIdentifierType } from '@skmtc/lang-typescript'
import denoJson from '../deno.json' with { type: 'json' }
export const ZodBase = toTsModelProjectionBase({
id: denoJson.name,
// House style: PascalCase with "Schema" suffix
toIdentifierName: ({ refName }) => `${capitalize(refName)}Schema`,
toIdentifierType: (): TsIdentifierType => ({ type: 'variable' }),
// House style: per-domain subdirectories
toExportPath: ({ refName }) => {
const domain = inferDomain(refName)
return `/${domain}/${refName}.schema.ts`
}
})
function inferDomain(refName: string): string {
if (refName.startsWith('User') || refName.startsWith('Auth')) return 'identity'
if (refName.startsWith('Order') || refName.startsWith('Cart')) return 'commerce'
return 'shared'
}Apply similar customizations to the form generator's import
paths, the TypeScript generator's interface vs type choice,
etc.
Share the cloned generators across projects
Reference the shared generators from each consumer project's
deno.json#imports:
// .skmtc/customer-app/deno.json
{
"imports": {
"@local/gen-zod": "../shared/gen-zod/mod.ts",
"@local/gen-typescript": "../shared/gen-typescript/mod.ts",
"@local/gen-shadcn-form": "../shared/gen-shadcn-form/mod.ts"
}
}Each consumer project imports the same shared source. Editing the shared generator updates all consumers after a rebundle.
Per-API enrichments
The consuming projects' client.json carries per-API
customization that goes into the shared schema's enrichment
fields:
// .skmtc/customer-app/.settings/client.json
{
"source": "https://api.example.com/customer-api.json",
"settings": {
"basePath": "packages/customer-app/src/generated",
"enrichments": {
"@local/gen-shadcn-form": {
"/users": {
"post": {
"main": { "title": "Sign up" }
}
}
}
}
}
}The shared generator's logic doesn't change per-consumer; only the enrichment data does.
Result
A team can add a new API to the design system by:
skmtc init <new-api>- Update
.skmtc/<new-api>/deno.json#importsto reference the shared generators - Set
.skmtc/<new-api>/.settings/client.json#sourceto the new API's spec - (Optional) Add per-operation enrichments
skmtc generate <new-api>
The output follows the house style automatically. No re-customization, no copy-paste of generator source.
Variations
- Private JSR scope. Publish the customized generators to
@yourorg/gen-*on a private JSR instance. Consumer projects install viaskmtc installlike any other JSR generator. Best when consumer projects live in different repos. - Git submodule. Add the customized generator source as a git submodule in each consumer repo. Each consumer pins to a specific submodule revision.
- Tiered house style. Have a small "core" of shared customizations and a larger "per-domain" layer that consumer projects override. Implemented via inheritance in the Projection classes (consumer project clones the shared clone and edits further).
- Generator-specific clone, generic schema. Sometimes only one or two generators need house-style customization; the rest stay as stock JSR. Mix-and-match: cloned customs for forms, stock for schemas.
Source
The "design system as cloned generator" pattern is the most mature SKMTC usage pattern. The key insight: customization belongs in source, not config. Config (enrichments) handles per-instance variation; cloning handles per-team variation.
See also
- How to change export paths
- How to change identifier conventions
- Clone vs install concept
- Why clone-to-customize
- Recipe: Multi-project monorepo — the user-side perspective on multi-project workspaces
Custom form field renderer
End-to-end example: clone gen-shadcn-form, add a date-picker field for schemas with format: 'date', see it in the generated forms.
What good generator code looks like
What reviewers look for in generator code: SKMTC vocabulary, producer boundaries, and the patterns the reference generators (gen-typescript, gen-zod, gen-valibot, gen-shadcn-form) settled on.