Zod Showcase

Ecosystem

Tools that work alongside Zod: framework integrations that put your schemas to work across the full stack, schema generators that stay in sync with your API specs, and monitoring integrations that make validation failures easier to debug in production.

Framework Integrations

Libraries that consume Zod schemas directly, putting the patterns from this course to work across the full stack. From API procedures to database queries to form handling.

tRPC ↗

An RPC framework for TypeScript that eliminates the REST/GraphQL layer entirely. Zod is tRPC's default input/output validator. Schemas you already know become end-to-end type-safe procedure contracts, with TypeScript inference flowing from server to client automatically.

Problem it solves

API types silently drifting between server and client is a whole category of runtime bugs. tRPC uses Zod schemas as the single source of truth for what a procedure accepts and returns, making those bugs a compile-time error instead.

Server: procedure definition
import { z } from 'zod';
import { router, publicProcedure } from './trpc';

export const userRouter = router({
  getById: publicProcedure
    .input(z.object({ id: z.number() }))
    .output(z.object({ id: z.number(), name: z.string(), email: z.email() }))
    .query(async ({ input }) => {
      // input.id is typed as number
      return db.users.findById(input.id);
    }),
});
Client: fully typed, no codegen
const user = await trpc.user.getById.query({ id: 1 });
// user is typed as { id: number; name: string; email: string }
// TypeScript infers this directly from your server schema

drizzle-zod ↗

Generates Zod schemas directly from your Drizzle ORM table definitions. Instead of writing both a Drizzle column spec and a matching Zod schema for the same data shape, you define the table once and derive the validator from it.

Problem it solves

In a typed backend, the same data shape appears in three places: database columns, Zod schema, TypeScript type. When you update a column, the other two silently drift. drizzle-zod removes the Zod schema from that list. It's generated automatically and always stays in sync.

Table definition
import { pgTable, serial, text, integer } from 'drizzle-orm/pg-core';

export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  name: text('name').notNull(),
  email: text('email').notNull(),
  age: integer('age'),
});
Generated schemas
import { createInsertSchema, createSelectSchema } from 'drizzle-zod';
import { users } from './schema';

// SELECT: all columns, matches DB shape exactly
const UserSchema = createSelectSchema(users);
// { id: number; name: string; email: string; age: number | null }

// INSERT: id omitted; override individual fields if needed
const NewUserSchema = createInsertSchema(users, {
  email: z.email(), // add stricter format validation
});

SvelteKit Superforms ↗

The dominant form library for SvelteKit. Pass a Zod schema to `superValidate()` and Superforms handles server-side validation in form actions, automatic `FormData` coercion, progressive enhancement, per-field error display, and taint detection (a browser warning before navigating away from unsaved changes).

Problem it solves

SvelteKit form actions receive raw FormData. All values are strings. You need to coerce types, validate, collect errors, and pass them back to the client. Superforms does all of this from a single Zod schema, removing the boilerplate that the Form Lab asked you to write by hand.

+page.server.ts
import { superValidate, fail } from 'sveltekit-superforms';
import { zod } from 'sveltekit-superforms/adapters';
import { z } from 'zod';

const schema = z.object({
  name: z.string().min(1),
  email: z.email(),
  age: z.coerce.number().int().min(18),
});

export const load = async () => ({
  form: await superValidate(zod(schema)),
});

export const actions = {
  default: async ({ request }) => {
    const form = await superValidate(request, zod(schema));
    if (!form.valid) return fail(400, { form });
    // form.data is fully typed: { name: string; email: string; age: number }
    return { form };
  }
};
+page.svelte
<!-- script block -->
import { superForm } from 'sveltekit-superforms';
let { data } = $props();
const { form, errors, enhance } = superForm(data.form);

<!-- template -->
<form method="POST" use:enhance>
  <input name="name" bind:value={$form.name} />
  {#if $errors.name}<span>{$errors.name[0]}</span>{/if}

  <input name="email" bind:value={$form.email} />
  {#if $errors.email}<span>{$errors.email[0]}</span>{/if}

  <button>Submit</button>
</form>

Code Generation

Generate Zod schemas from an OpenAPI spec so they stay in sync with the API contract. No manual schema writing needed.

Orval ↗

Generates fully-typed API clients (Axios, Fetch, React Query, and more) from an OpenAPI spec. Setting the client to "zod" outputs Zod schemas instead of plain TypeScript interfaces.

Problem it solves

Maintaining handwritten Zod schemas for a large API is tedious, and they drift from the spec whenever the API changes. Orval re-generates them from the OpenAPI spec on demand.

orval.config.ts
import { defineConfig } from 'orval';

export default defineConfig({
  petstore: {
    input: './openapi.yaml',
    output: {
      target: './src/api/petstore.ts',
      // 'zod' mode emits Zod schemas instead of TS interfaces
      client: 'zod',
    },
  },
});
Generated output
// src/api/petstore.ts (generated. Do not edit)
import { z } from 'zod';

export const petSchema = z.object({
  id: z.number().int(),
  name: z.string(),
  status: z.enum(['available', 'pending', 'sold']).optional(),
});

export type Pet = z.infer<typeof petSchema>;

openapi-zod-client ↗

A lightweight CLI that reads an OpenAPI spec and outputs Zod schemas plus a typed HTTP client using Zodios. No config file needed.

Problem it solves

When you want Zod schemas and a typed endpoint client without a heavy generator framework. A single command takes your spec in and spits a ready-to-use client out.

CLI
npx openapi-zod-client ./openapi.yaml -o ./src/api/client.ts
Generated output
// src/api/client.ts (generated. Do not edit)
import { makeApi, Zodios } from '@zodios/core';
import { z } from 'zod';

const Pet = z.object({
  id: z.number().int().optional(),
  name: z.string(),
  status: z.enum(['available', 'pending', 'sold']).optional(),
});

const api = makeApi([
  {
    method: 'get',
    path: '/pets/:id',
    alias: 'getPetById',
    response: Pet,
  },
]);

export const client = new Zodios('/api', api);

Hey API / openapi-ts ↗

A plugin-based OpenAPI TypeScript codegen tool. The Zod plugin generates Zod schemas alongside TypeScript types and service clients, all from a single config file.

Problem it solves

When you need a full-featured, extensible codegen pipeline that can grow with your project. Plugins let you opt into exactly what you need: types, services, Zod schemas, or all three.

openapi-ts.config.ts
import { defineConfig } from '@hey-api/openapi-ts';

export default defineConfig({
  input: './openapi.yaml',
  output: './src/client',
  plugins: [
    '@hey-api/typescript',
    {
      name: '@hey-api/zod',
      output: 'schemas',
    },
  ],
});
Generated output
// src/client/schemas.ts (generated. Do not edit)
import { z } from 'zod';

export const zPet = z.object({
  id: z.number().int().optional(),
  name: z.string(),
  status: z.enum(['available', 'pending', 'sold']).optional(),
});

Observability

Get structured context when validation fails in production, not just a generic error message.

Sentry - zodErrorsIntegration ↗

Enhances Sentry error reporting for apps using Zod. When a ZodError is captured, the integration attaches the full list of validation issues as structured data on the Sentry event: field paths, error codes, and the values that were received.

Problem it solves

Without this integration, Sentry shows only the top-level ZodError message, which tells you validation failed but not where or why. With it, every failed parse in production is fully debuggable: you see exactly which fields failed, what was expected, and what was received.

Sentry init
import * as Sentry from '@sentry/browser'; // or @sentry/node

Sentry.init({
  dsn: 'YOUR_DSN',
  integrations: [
    Sentry.zodErrorsIntegration({
      // Max number of Zod issues inlined per event (default: 10)
      limit: 10,
      // Save the full issue list as a JSON attachment (default: false)
      saveZodIssuesAsAttachment: false,
    }),
  ],
});
Error context in Sentry
[
  {
    "code": "too_small",
    "path": ["name"],
    "message": "Name is required",
    "minimum": 1,
    "type": "string",
    "inclusive": true,
    "received": ""
  },
  {
    "code": "invalid_string",
    "path": ["email"],
    "message": "Invalid email format",
    "validation": "email",
    "received": "not-an-email"
  }
]