"TypeScript makes it type-safe" is the sentence interviewers wait for so they can ask "at runtime too?". Types disappear at compile time; anything that enters the program from outside, HTTP bodies, query strings, environment variables, storage, third-party APIs, is unknown until something checks it. The senior pattern is schemas at the boundary that both validate and define the types.
localStorage/cookies, environment variables, webhooks, messages from workers or sockets. Inside the boundary, types are trustworthy because the boundary enforced them.z.object, z.infer, parse/safeParse, transforms, refinements, z.input vs z.output), valibot (modular and tree-shakable, ideal for client bundles), ArkType (TypeScript-syntax strings, very fast). All implement Standard Schema, a common interface that Nuxt UI forms, VeeValidate, h3 (readValidatedBody), TanStack and others accept, so the choice is per project, not per library integration.type Product = z.infer<typeof productSchema>; the schema is the single source of truth and the type follows. Schemas live in shared/schemas so the Nuxt server route and the page import the same one.readValidatedBody(event, schema.parse), getValidatedQuery, getValidatedRouterParams on the server (throw 400 with issues); useRuntimeConfig values at startup in a Nitro plugin; API responses on the client only when the API is not yours (your own routes are typed end to end); form input through the form library.safeParse versus parse: parse throws a ZodError; safeParse returns { success, data | error } for flows where failure is expected (user input). Format issues for users with flatten()/treeifyError and for logs with the raw issue list.UserId, Email, NonEmptyString; schemas produce them (z.string().email().brand<'Email'>()), so a validated value carries proof of validation in its type.never in switch defaults and satisfies Record<Union, ...> for lookup tables, so adding a variant is a compile error everywhere it matters.unknown. catch (e) is unknown under useUnknownInCatchVariables; narrow with instanceof, a type guard, or Error.isError (ES2026). Model expected failures as data (Result<T, E> discriminated unions) and reserve exceptions for the unexpected; FetchError from ofetch carries status, statusText and typed data.process.env/runtimeConfig once at startup with a schema and fail fast; never as string an env variable.The boundary pattern end to end in Nuxt:
import { z } from 'zod'
export const productSchema = z.object({
id: z.string().uuid().brand<'ProductId'>(),
name: z.string().trim().min(1).max(120),
status: z.enum(['draft', 'published', 'archived']),
priceMinor: z.number().int().nonnegative(),
tags: z.array(z.string()).max(10).default([]),
})
export type Product = z.output<typeof productSchema>
export type ProductId = Product['id']
export const createProductSchema = productSchema.omit({ id: true })
export type CreateProductInput = z.input<typeof createProductSchema> // tags optional on input, required on output
import { createProductSchema } from '#shared/schemas/product'
export default defineEventHandler(async (event) => {
const input = await readValidatedBody(event, createProductSchema.parse) // 400 with issues in `data` on failure
return useDb().products.create(input) // input is typed and *validated*; no cast anywhere
})
import { z } from 'zod'
const envSchema = z.object({
stripeSecret: z.string().min(1),
session: z.object({ maxAge: z.number().int().positive() }),
})
export default defineNitroPlugin(() => {
// Fail fast at startup instead of discovering a missing secret on the first checkout.
envSchema.parse(useRuntimeConfig())
})
Result types and typed errors on the client, plus a third-party API validated because it is not ours:
export type Result<T, E = { status: number, message: string }> = { ok: true, value: T } | { ok: false, error: E }
export async function tryFetch<T>(schema: { parse: (v: unknown) => T }, url: string): Promise<Result<T>> {
try {
const raw: unknown = await $fetch(url)
return { ok: true, value: schema.parse(raw) } // validated: the third party changed a field? we learn here, not in a template
}
catch (e) {
if (Error.isError(e) && 'status' in e) return { ok: false, error: { status: Number(e.status ?? 500), message: e.message } }
return { ok: false, error: { status: 500, message: 'Unexpected error' } }
}
}
Exhaustive lookup tables that fail to compile when a variant is added:
import type { Product } from '#shared/schemas/product'
export const STATUS_LABEL = {
draft: 'Draft',
published: 'Live',
archived: 'Archived',
} satisfies Record<Product['status'], string> // add 'scheduled' to the schema and this line errors
Re-parsing your own typed /api response on the client wastes CPU and hides the real seam. Your own routes are typed end to end by Nuxt; validate their inputs on the server and validate external sources on whichever side receives them.
const p = (await $fetch(url)) as Product compiles and proves nothing; when the upstream renames a field, the failure surfaces as undefined in a template three components later. A parse fails at the boundary with the field name in the error.
The current zod major and its brand/treeifyError API names, and the Standard Schema adopter list (Nuxt UI, VeeValidate, h3) as of the interview week.
curl and read the 400 payload, then break the schema on purpose to see the page's compile error.NUXT_STRIPE_SECRET; confirm it refuses to start with a clear message.as cast on external data in a project with safeParse and log what the first real failure reveals."By validating at the boundaries and deriving types from the validators. A schema in shared/schemas, zod or valibot, defines each input once; the server route parses bodies and params with readValidatedBody, so invalid input becomes a 400 with field-level issues, and the handler's return type flows to useFetch through Nuxt's generated route types, so my own API is typed end to end without casts. External data, third-party APIs, storage, environment variables, gets parsed where it enters, and config is validated once at startup so a missing secret fails the boot rather than the first checkout. Inside those boundaries the types are trustworthy, and I keep them honest with unknown for anything unchecked, branded types for validated ids, and exhaustive switches so new variants cannot be forgotten."
Nothing yet. Notes filed here show up on this page and the section overview.
Modules, declarations and configuration
The tsconfig options that matter in a Vite and Nuxt project, module resolution and verbatimModuleSyntax, project references, writing and shipping declaration files, ambient and module augmentation, Node's native type stripping, and what TypeScript 6 and 7 changed.
Typing patterns and smells
[object Object]