skills/practices/shared-contracts/SKILL.md
Patterns for sharing types, API contracts, and validation schemas between frontend and backend. Use when multiple domains consume the same data shapes to prevent contract drift.
npx skillsauth add devjarus/coding-agent shared-contractsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
packages/shared/, lib/shared/, or src/types/api.ts in a monorepo)z.infer<typeof schema> for TypeScript types — single source of truth for type AND validationDefine the schema once in a shared location. Both the API route and the frontend import from it — the schema drives runtime validation on the server and form validation + TypeScript types on the client.
// packages/shared/src/schemas/user.ts
import { z } from "zod"
export const CreateUserSchema = z.object({
email: z.string().email(),
name: z.string().min(1).max(100),
role: z.enum(["admin", "member", "viewer"]),
})
export type CreateUserInput = z.infer<typeof CreateUserSchema>
// backend: apps/api/src/routes/users.ts
import { CreateUserSchema } from "@myapp/shared/schemas/user"
app.post("/users", async (req, res) => {
const result = CreateUserSchema.safeParse(req.body)
if (!result.success) {
return res.status(400).json({ error: result.error.flatten() })
}
// result.data is fully typed as CreateUserInput
await createUser(result.data)
res.status(201).json({ ok: true })
})
// frontend: apps/web/src/features/users/CreateUserForm.tsx
import { CreateUserSchema, type CreateUserInput } from "@myapp/shared/schemas/user"
function CreateUserForm() {
const { register, handleSubmit } = useForm<CreateUserInput>({
resolver: zodResolver(CreateUserSchema), // same schema, client-side validation
})
// ...
}
Define the OpenAPI spec as the single source of truth. Generate server stubs and client types from it so both sides are always in sync — no manual type duplication.
# openapi.yaml
paths:
/users:
post:
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/CreateUserInput"
responses:
"201":
content:
application/json:
schema:
$ref: "#/components/schemas/User"
# generate server types
npx openapi-typescript openapi.yaml --output src/types/api.d.ts
# generate client (e.g., with orval or openapi-fetch)
npx orval --input openapi.yaml --output src/api/client.ts
Both the backend route handler and the frontend API client are typed from the same spec. A breaking change to the spec surfaces as a TypeScript error in both.
The router definition IS the contract. The frontend gets a type-safe client automatically — no codegen step, no manual types, no drift possible.
// server/routers/user.ts
export const userRouter = router({
create: publicProcedure
.input(
z.object({
email: z.string().email(),
name: z.string().min(1),
})
)
.mutation(async ({ input }) => {
return db.user.create({ data: input })
}),
})
// frontend component
const { mutate } = trpc.user.create.useMutation()
// input is fully typed — TypeScript errors if shape drifts
mutate({ email: "[email protected]", name: "Alice" })
| Anti-pattern | Why it's harmful |
|---|---|
| Duplicating types in frontend/ and backend/ separately | The two copies drift immediately. Renaming a field on one side silently breaks the other. |
| Using any at API boundaries | Defeats TypeScript entirely. Runtime shape mismatches become silent bugs. |
| Changing backend response without updating the frontend consumer | The contract is broken the moment the PR merges. Only caught at runtime or in E2E tests. |
| TypeScript types only — no runtime validation | Types disappear at compile time. An unexpected server payload or a bad environment variable causes a runtime crash with no useful error. |
testing
Multi-source research method — decompose a question, fan out parallel investigators, interleaved-think each result, verify claims adversarially, synthesize a cited answer. Use for breadth-heavy research, stack comparisons, "which approach wins" questions.
testing
Decide when to use unit vs integration vs e2e tests, and when to mock vs use the real thing per dependency. Dependency injection is the enabler — without it you end up monkey-patching imports. Apply when writing tests of any kind.
development
Test-driven development process — write failing test, implement to pass, refactor. Use when implementing any feature or fixing bugs.
tools
Patterns for apps with external clients — singleton clients, factories, connection pools, retry/timeout wrappers, graceful shutdown, service-layer organization. Apply when an app talks to DBs/APIs/caches/queues.