From coding-agent
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 claudepluginhub devjarus/coding-agentThis skill uses the workspace's default tool permissions.
- Project has both frontend and backend in the same repo
Generates design tokens/docs from CSS/Tailwind/styled-components codebases, audits visual consistency across 10 dimensions, detects AI slop in UI.
Records polished WebM UI demo videos of web apps using Playwright with cursor overlay, natural pacing, and three-phase scripting. Activates for demo, walkthrough, screen recording, or tutorial requests.
Delivers idiomatic Kotlin patterns for null safety, immutability, sealed classes, coroutines, Flows, extensions, DSL builders, and Gradle DSL. Use when writing, reviewing, refactoring, or designing Kotlin code.
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: "a@b.com", 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. |