Domain-Driven Design with Zod
One of the most underrated patterns in TypeScript development is using Zod not just for validation, but as a Design by Contract mechanism.
The Problem with Runtime Surprises
In a statically typed language, you’d expect type errors to surface at compile time. But when data crosses the boundary from external sources — API responses, MDX frontmatter, form inputs — TypeScript’s compile-time guarantees break down.
// This compiles fine, but might fail at runtime
const article = await getArticle(slug) // Type: Article | null
article.title // What if article is null?
Zod as Your Contract Enforcement Layer
import { z } from 'zod'
export const ArticleSchema = z.object({
slug: z.string().min(1).regex(/^[a-z0-9-]+$/),
title: z.string().min(1).max(120),
publishedAt: z.date(),
tags: z.array(z.string().min(1)).min(1),
})
export type Article = z.infer<typeof ArticleSchema>
The key insight: the schema is the contract. If the data doesn’t conform, it throws immediately at the boundary — not deep inside your business logic.
Build Time vs Runtime
With Astro Content Collections, Zod validation runs at build time for MDX frontmatter. A malformed frontmatter field fails the build, preventing invalid data from ever reaching production.
That’s the dream: turning runtime surprises into compile-time certainties.