skmtcdocs

Full-stack TypeScript app

Generate types, validators, query hooks, mocks, and forms from one OpenAPI document — the standard SKMTC combination for a TypeScript/React app.

This recipe exists for one moment: add a field to the schema, regenerate, and watch the type, the validator, and the form update together while everything that imports them stays consistent. The last section stages that moment; the rest builds the stack that makes it possible.

What you'll build

A src/generated/ tree containing:

  • TypeScript types matching every schema component
  • Zod validators for those same schemas
  • Tanstack Query hooks (useQuery/useMutation) for every operation
  • MSW handlers for development-time mocking
  • shadcn/ui form components for mutating operations with object bodies

All five generators converge on shared definitions — the engine produces each schema exactly once, regardless of how many generators need it.

Stack

  • Vite or Next.js (your existing app, no SKMTC-runtime needed)
  • React + Tanstack Query + shadcn/ui
  • MSW (for dev-only mocks)

Generators used

Types: @skmtc/gen-typescript

Produces export type Pet = {...} per schema component. The foundation everything else references.

Validators: @skmtc/gen-zod

Produces export const pet = z.object({...}) per schema component. Used at runtime boundaries (API responses, form submissions).

Query hooks: @skmtc/gen-tanstack-query-fetch-zod

Produces useQuery for GET/DELETE and useMutation for POST/PUT/PATCH (when there's a request body). Hooks call fetch directly — typical first edit when cloned is replacing fetch with a team-specific wrapper.

Mocks: @skmtc/gen-msw

Produces per-operation http.get/http.post handlers plus a shared toRoutesList(deps) factory. Wired into the app's MSW worker for dev-mode mocking.

Forms: @skmtc/gen-shadcn-form

Produces a React form for every POST/PUT/PATCH operation with an object body. The form references both the Zod schema (for validation) and the mutation hook (for submit) — both already generated by the upstream generators.

Setup

skmtc init myapp src/generated
skmtc install @skmtc/gen-typescript myapp
skmtc install @skmtc/gen-zod myapp
skmtc install @skmtc/gen-tanstack-query-fetch-zod myapp
skmtc install @skmtc/gen-msw myapp
skmtc install @skmtc/gen-shadcn-form myapp

Edit .skmtc/myapp/.settings/client.json:

{
  "source": "https://api.example.com/openapi.json",
  "settings": {
    "basePath": "src/generated"
  }
}

Generate:

skmtc generate myapp

Step-by-step

What ends up where in src/generated/ (each generator's toExportPath claims its own subtree):

src/generated/
├── types/
│   ├── pet.generated.ts            # type + Zod schema (both generators converge on this file)
│   ├── order.generated.ts          # ditto
│   └── ...
├── services/
│   ├── useGetPetById.generated.ts  # query hook
│   ├── useAddPet.generated.ts      # mutation hook
│   └── ...
├── forms/
│   ├── AddPet.generated.tsx        # wired React form
│   └── ...
└── mocks/
    └── handlers.generated.ts       # all MSW handlers + toRoutesList factory

The hooks file imports from the types/Zod file:

// src/generated/services/useGetPetById.generated.ts
import { pet, type Pet } from '../types/pet.generated.ts'
import { useQuery } from '@tanstack/react-query'

export const useGetPetById = (args: { petId: number }) => useQuery({...})

The forms reference the hook and the Zod schema:

// src/generated/forms/AddPet.generated.tsx
import { pet } from '../types/pet.generated.ts'
import { useAddPet } from '../services/useAddPet.generated.ts'
// ...

Everything composes because all five generators share the (name, exportPath) cache. See cross-generator coordination concept.

Result

In your app:

import { useGetPetById } from '@/generated/services/useGetPetById.generated.ts'
import { AddPetForm } from '@/generated/forms/AddPet.generated.tsx'

export function PetDetailPage({ id }: { id: number }) {
  const { data } = useGetPetById({ petId: id })
  return (
    <div>
      <h1>{data?.name}</h1>
      <AddPetForm />
    </div>
  )
}

Set up MSW in development:

// src/setupMsw.ts
import { setupWorker } from 'msw/browser'
import { toRoutesList } from '@/generated/mocks/handlers.generated.ts'

const worker = setupWorker(...toRoutesList({ store: yourMockStore }))

Variations

  • Swap fetch transport. Clone gen-tanstack-query-fetch-zod to use your team's apiFetch wrapper. See how to swap a peer dependency.
  • Supabase backend. Use @skmtc/gen-tanstack-query-supabase-zod instead of the fetch variant. Add gen-shadcn-select and gen-shadcn-table for the search/list UI components that pair with it.
  • Add a table. Install @skmtc/gen-shadcn-table for list-GET operations.

The payoff: change the schema

Add one field to a schema component in your OpenAPI document — say nickname: { type: string } on Pet — and regenerate:

skmtc generate <project>
git diff --stat src/generated/

The diff touches every file that spells out Pet's shape: the type gains a field, the validator gains a rule, the form gains an input. The hooks and mocks that import those definitions stay consistent without changing, because they reference the shared artifacts rather than duplicating them (see cross-generator coordination). One schema edit, one regenerate, a fully consistent stack — this property is what the recipe exists to demonstrate.

Source

This recipe's stack is the most common SKMTC usage pattern. Real projects typically clone one or two of the generators (the fetch wrapper, the form's submit-flow) to match team conventions. The schema-level generators (gen-typescript, gen-zod, gen-msw) usually run unmodified.

See also

On this page