Skip to content
rsdlt.dev
Go back

ArkType vs Zod: Why I Standardized on Zod Across Forms, API and Database

A month ago, Foambyte’s login form validated with ArkType, and the first server schemas used drizzle-arktype. Last week I replaced both with Zod. ArkType is an excellent library: fast, with a concise, TypeScript-like syntax. The switch wasn’t about the library itself. It was about where validation has to plug in.

This post is about choosing a validation library at the architecture level, and why integrations beat benchmarks.

Where validation lives in a full-stack TypeScript app

In Foambyte, the same data shape is checked in at least four places:

  1. Database: schemas derived from the Drizzle tables, for inserts and updates.
  2. API input: every request body and parameter validated before a handler runs.
  3. API documentation: the OpenAPI spec that describes those same shapes.
  4. Forms: field validation in the React app, with translated error messages.

If each layer uses a different library, or a library that some layers don’t support, you end up maintaining the same rules twice and hoping they agree.

The evaluation

When I wired up the API’s OpenAPI documentation, I briefly tried the options side by side: ArkType’s Hono validator, Valibot with drizzle-valibot, and the third-party hono-openapi package, next to @hono/zod-openapi and drizzle-zod.

The comparison came down to one question: which library has first-class support at every layer?

LayerZodArkType (at the time)
Drizzle table → schemadrizzle-zoddrizzle-arktype
Hono routes + OpenAPI docs@hono/zod-openapi (official, typed routes and docs from one definition)Validator only; docs need a separate tool
TanStack FormSupported (Standard Schema)Supported (Standard Schema)

The deciding row is the middle one. @hono/zod-openapi gives me one route definition that validates the request, types the handler and generates the OpenAPI spec. Nothing else on the list matched that without stitching tools together.

Before and after

The login form’s validators show how small the switch was, because TanStack Form accepts any Standard Schema library:

// Before: ArkType
const Email = type("string.email").configure({
  message: () => intl.formatMessage({ id: "login.email.error", defaultMessage: "Must be a valid email address." }),
});
// validators={{ onBlur: Email }}
// After: Zod 4
import { z } from "zod/v4";

validators={{
  onBlur: z.email({ error: intl.formatMessage({ id: "login.email.error", defaultMessage: "Must be a valid email address." }) }),
}}

On the server, the table becomes the source of the schemas:

export const sBrewHouseSystemSchema = createSelectSchema(brewHouseSystemsTable);
export const iBrewHouseSystemSchema = createInsertSchema(brewHouseSystemsTable);
export const uBrewHouseSystemSchema = createUpdateSchema(brewHouseSystemsTable);

Those schemas feed straight into route definitions, so a column added to the table shows up in validation, handler types and docs at once.

Tip

Standard Schema made this migration cheap. Because TanStack Form speaks the Standard Schema interface, swapping libraries in forms was a local change. Prefer tools that accept Standard Schema: they keep the validation-library decision reversible.

What I gave up

Why Zod 4

Zod 4 landed at the right time. It’s significantly faster than Zod 3, ships smaller builds, and has top-level formats like z.email() and a unified error option for messages. That made the performance gap with ArkType smaller, and the error API fits react-intl translations cleanly.

Takeaways


I’m Rodrigo Santiago, a product leader who builds. I co-founded Foambyte with my brother Diego, a brewmaster who leads the product as CPO, and I designed and built the platform: architecture, frontend, backend, mobile and infrastructure. If you need someone who can own both the product and the code, let’s talk on LinkedIn.


Share this post:

Previous Post
Testing Brewing Math: Unit Tests for IBU, Color and Carbonation Calculators
Next Post
Choosing Authentication for a Web + Mobile SaaS: Why better-auth