<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged zod</title>
<link>https://coderlegion.com/tag/zod</link>
<description>Powered by Question2Answer</description>
<item>
<title>Zod vs Yup in 2026: Which Validation Library Should You Use?</title>
<link>https://coderlegion.com/25116/zod-vs-yup-in-2026-which-validation-library-should-you-use</link>
<description>&lt;p&gt;You've got a signup form. Email, password, maybe a checkbox for terms and conditions. Simple, right? Then someone submits &lt;code&gt;email: 123&lt;/code&gt; and your API route crashes with a cryptic error three files deep, because nothing ever checked that the data coming in actually matched what your code expected.&lt;/p&gt;
&lt;p&gt;That's the exact gap schema validation libraries fill — and if you've searched for one, you've almost certainly landed on two names: &lt;strong&gt;Zod&lt;/strong&gt; and &lt;strong&gt;Yup&lt;/strong&gt;. Both let you describe the shape of your data once and validate real values against it. Both work great with React Hook Form. Both have been around long enough to be genuinely production-tested.&lt;/p&gt;
&lt;p&gt;But they're not interchangeable, and picking the wrong one for your setup means either fighting TypeScript for months or hand-writing types you shouldn't have to. By the end of this guide, you'll know exactly how Zod and Yup differ, see real code for both, and know which one actually fits your project — not just which one has more GitHub stars.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;quick-answer&quot;&gt;Quick Answer&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Choose Zod&lt;/strong&gt; if you're using TypeScript and want your schema to &lt;em&gt;be&lt;/em&gt; your type — no writing an interface separately and hoping it stays in sync.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Choose Yup&lt;/strong&gt; if you're maintaining an existing Formik-based codebase, or your team is more comfortable with its &lt;code&gt;.shape()&lt;/code&gt; API and mature async validation methods.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For any brand-new TypeScript project in 2026, Zod is the safer long-term default — it's what most modern tools (React Hook Form, tRPC, Next.js Server Action patterns) assume you're using. Now let's get into why.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-is-schema-validation-exactly-&quot;&gt;What Is Schema Validation, Exactly?&lt;/h3&gt;
&lt;p&gt;Before comparing the two, a quick beginner-friendly definition: a &lt;strong&gt;schema&lt;/strong&gt; is just a description of what your data &lt;em&gt;should&lt;/em&gt; look like — &quot;this field is a string,&quot; &quot;this field is a number between 1 and 100,&quot; &quot;this field is a valid email.&quot; A &lt;strong&gt;validation library&lt;/strong&gt; takes that schema and checks real, incoming data (a form submission, an API request body) against it, telling you exactly what's wrong if it doesn't match.&lt;/p&gt;
&lt;p&gt;Without one, you end up writing manual checks like this everywhere:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;if (typeof data.email !== &quot;string&quot; || !data.email.includes(&quot;@&quot;)) {
  throw new Error(&quot;Invalid email&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Multiply that across every field, every form, every API route, and it gets messy fast — inconsistent error messages, duplicated logic, and no single source of truth for what &quot;valid&quot; even means. Zod and Yup both solve this by letting you define the rules once and reuse them everywhere.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;zod-typescript-first-validation&quot;&gt;Zod: TypeScript-First Validation&lt;/h3&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://zod.dev&quot;&gt;Zod&lt;/a&gt; was built specifically for TypeScript projects. Its core idea: you write one schema, and Zod derives both the runtime validator &lt;em&gt;and&lt;/em&gt; the TypeScript type from it — there's no second place to keep in sync.&lt;/p&gt;
&lt;h4 id=&quot;installing-zod&quot;&gt;Installing Zod&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install zod
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;a-basic-zod-schema&quot;&gt;A Basic Zod Schema&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// lib/validators/user-schema.ts
import { z } from &quot;zod&quot;;

export const userSchema = z.object({
  name: z.string().min(2, &quot;Name must be at least 2 characters&quot;),
  email: z.email(&quot;Please enter a valid email address&quot;),
  age: z.number().int().positive().optional(),
  website: z.url().optional(),
});

// The type is derived directly from the schema — never written twice
export type User = z.infer&amp;lt;typeof userSchema&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; &lt;code&gt;z.object({...})&lt;/code&gt; describes the shape. &lt;code&gt;z.email()&lt;/code&gt; and &lt;code&gt;z.url()&lt;/code&gt; are Zod's built-in format validators (as of Zod v4, these live as top-level helpers rather than chained &lt;code&gt;.email()&lt;/code&gt; string methods, though the chained form still works). &lt;code&gt;z.infer&amp;lt;typeof userSchema&amp;gt;&lt;/code&gt; pulls a real TypeScript type straight out of the schema — if you add a field to the schema, the &lt;code&gt;User&lt;/code&gt; type updates automatically, with zero manual work.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; This is Zod's single biggest selling point. You will never end up with a TypeScript interface that's quietly out of sync with your actual validation rules, because there's only one definition.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;validating-data-with-zod&quot;&gt;Validating Data with Zod&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// Somewhere handling a form submission or API request
const result = userSchema.safeParse({
  name: &quot;Ali&quot;,
  email: &quot;not-an-email&quot;,
});

if (!result.success) {
  console.log(result.error.flatten().fieldErrors);
  // { email: [&quot;Please enter a valid email address&quot;] }
} else {
  // result.data is fully typed as `User`
  console.log(result.data.name);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; &lt;code&gt;safeParse()&lt;/code&gt; never throws — it returns a &lt;code&gt;{ success: true, data }&lt;/code&gt; or &lt;code&gt;{ success: false, error }&lt;/code&gt; object, so you don't need a &lt;code&gt;try/catch&lt;/code&gt; block for normal validation flow. &lt;code&gt;error.flatten().fieldErrors&lt;/code&gt; gives you a clean, per-field map of error messages, perfect for showing next to form inputs.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Common Mistake:&lt;/strong&gt; Using &lt;code&gt;.parse()&lt;/code&gt; instead of &lt;code&gt;.safeParse()&lt;/code&gt; in a code path where invalid data is expected (like a form submission). &lt;code&gt;.parse()&lt;/code&gt; &lt;em&gt;throws&lt;/em&gt; on failure, which means you need a &lt;code&gt;try/catch&lt;/code&gt; everywhere — &lt;code&gt;.safeParse()&lt;/code&gt; is almost always the better default for user input.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;zod-in-a-next-js-server-action&quot;&gt;Zod in a Next.js Server Action&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// actions/create-user.ts
&quot;use server&quot;;

import { userSchema } from &quot;@/lib/validators/user-schema&quot;;

export async function createUser(prevState: unknown, formData: FormData) {
  const result = userSchema.safeParse({
    name: formData.get(&quot;name&quot;),
    email: formData.get(&quot;email&quot;),
  });

  if (!result.success) {
    return { errors: result.error.flatten().fieldErrors };
  }

  // result.data is typed and safe to use here
  // await db.user.create({ data: result.data });

  return { success: true };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is the exact pattern you'll see across most modern Next.js codebases — validate raw &lt;code&gt;FormData&lt;/code&gt; on the server, never trust the client, and let the schema do the type narrowing for you.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;yup-the-established-forms-favorite&quot;&gt;Yup: The Established Forms Favorite&lt;/h3&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/jquense/yup&quot;&gt;Yup&lt;/a&gt; predates the current TypeScript-first wave of tooling. It grew up alongside &lt;strong&gt;Formik&lt;/strong&gt; and is still deeply embedded in a lot of React form codebases, particularly ones started before 2022.&lt;/p&gt;
&lt;h4 id=&quot;installing-yup&quot;&gt;Installing Yup&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install yup
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;a-basic-yup-schema&quot;&gt;A Basic Yup Schema&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// lib/validators/user-schema.ts
import * as yup from &quot;yup&quot;;

export const userSchema = yup.object({
  name: yup.string().min(2, &quot;Name must be at least 2 characters&quot;).required(),
  email: yup.string().email(&quot;Please enter a valid email address&quot;).required(),
  age: yup.number().integer().positive(),
  website: yup.string().url(),
});

// Yup's type inference — functional, but less precise than Zod's
export type User = yup.InferType&amp;lt;typeof userSchema&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; the shape looks similar to Zod at a glance, but notice &lt;code&gt;.required()&lt;/code&gt; — in Yup, fields are &lt;strong&gt;optional by default&lt;/strong&gt; unless you explicitly mark them required, which is the opposite of Zod's default (Zod fields are required unless you call &lt;code&gt;.optional()&lt;/code&gt;). This trips up a lot of developers switching between the two.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Common Mistake:&lt;/strong&gt; Forgetting &lt;code&gt;.required()&lt;/code&gt; on a Yup field and assuming it behaves like Zod. Yup schemas silently allow &lt;code&gt;undefined&lt;/code&gt; on any field you don't explicitly mark required — this is one of the most common Yup-related bugs in production forms.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;validating-data-with-yup&quot;&gt;Validating Data with Yup&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;try {
  const validData = await userSchema.validate(
    { name: &quot;Ali&quot;, email: &quot;not-an-email&quot; },
    { abortEarly: false }
  );
  console.log(validData);
} catch (err) {
  if (err instanceof yup.ValidationError) {
    console.log(err.errors); // [&quot;Please enter a valid email address&quot;]
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; Yup's &lt;code&gt;.validate()&lt;/code&gt; is &lt;strong&gt;async by default&lt;/strong&gt; and returns a Promise — this is a real difference from Zod, where &lt;code&gt;safeParse&lt;/code&gt; is synchronous unless you specifically opt into &lt;code&gt;safeParseAsync&lt;/code&gt;. &lt;code&gt;abortEarly: false&lt;/code&gt; tells Yup to collect every validation error instead of stopping at the first one. For a version that never throws, Yup also offers &lt;code&gt;validateSync()&lt;/code&gt; with a &lt;code&gt;try/catch&lt;/code&gt;, or you can check &lt;code&gt;.isValid()&lt;/code&gt;.&lt;/p&gt;
&lt;h4 id=&quot;yup-in-a-next-js-server-action&quot;&gt;Yup in a Next.js Server Action&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// actions/create-user.ts
&quot;use server&quot;;

import { userSchema } from &quot;@/lib/validators/user-schema&quot;;
import { ValidationError } from &quot;yup&quot;;

export async function createUser(prevState: unknown, formData: FormData) {
  try {
    const data = await userSchema.validate(
      {
        name: formData.get(&quot;name&quot;),
        email: formData.get(&quot;email&quot;),
      },
      { abortEarly: false }
    );

    // await db.user.create({ data });
    return { success: true };
  } catch (err) {
    if (err instanceof ValidationError) {
      const fieldErrors: Record&amp;lt;string, string[]&amp;gt; = {};
      for (const e of err.inner) {
        if (e.path) fieldErrors[e.path] = [...(fieldErrors[e.path] ?? []), e.message];
      }
      return { errors: fieldErrors };
    }
    throw err;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice this requires a &lt;code&gt;try/catch&lt;/code&gt; and manually reshaping &lt;code&gt;err.inner&lt;/code&gt; into a field-keyed object — Zod's &lt;code&gt;flatten().fieldErrors&lt;/code&gt; gives you that shape for free.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;zod-vs-yup-side-by-side&quot;&gt;Zod vs Yup: Side-by-Side&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;Zod &lt;/th&gt;&lt;th&gt;Yup&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;TypeScript inference&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Automatic, precise (&lt;code&gt;z.infer&lt;/code&gt;) &lt;/td&gt;&lt;td&gt;Functional but looser (&lt;code&gt;InferType&lt;/code&gt;)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Default field behavior&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Required by default &lt;/td&gt;&lt;td&gt;Optional by default&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Validation style&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Sync by default (&lt;code&gt;safeParse&lt;/code&gt;), async available &lt;/td&gt;&lt;td&gt;Async by default (&lt;code&gt;.validate()&lt;/code&gt;)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Error shape&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Structured &lt;code&gt;ZodError&lt;/code&gt; with &lt;code&gt;.flatten()&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;ValidationError&lt;/code&gt; with &lt;code&gt;.inner&lt;/code&gt; array&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Bundle size (gzipped)&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Zod is slightly larger, but the difference &amp;lt;br/&amp;gt; is usually too small to matter for real &amp;lt;br/&amp;gt; applications. &lt;/td&gt;&lt;td&gt;Slightly smaller&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;React Hook Form support&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;✅ via &lt;code&gt;@hookform/resolvers/zod&lt;/code&gt; &lt;/td&gt;&lt;td&gt;✅ via &lt;code&gt;@hookform/resolvers/yup&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Ecosystem fit&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;tRPC, modern Next.js patterns, Server Actions &lt;/td&gt;&lt;td&gt;Formik, legacy React form codebases&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Learning curve&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Slightly more API surface &lt;/td&gt;&lt;td&gt;Simpler, more forgiving for JS-first teams&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Best for&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;New TypeScript projects &lt;/td&gt;&lt;td&gt;Existing Formik/Yup codebases&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; Bundle size differences here are small enough that they shouldn't be your deciding factor for most apps — a few KB gzipped rarely matters next to your actual app code and images.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;using-either-one-with-react-hook-form&quot;&gt;Using Either One with React Hook Form&lt;/h3&gt;
&lt;p&gt;This is where a lot of real-world decisions actually get made, so it's worth showing both side by side. Both integrate through the same &lt;code&gt;@hookform/resolvers&lt;/code&gt; package.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install react-hook-form @hookform/resolvers
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;With Zod:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&quot;use client&quot;;

import { useForm } from &quot;react-hook-form&quot;;
import { zodResolver } from &quot;@hookform/resolvers/zod&quot;;
import { z } from &quot;zod&quot;;

const formSchema = z.object({
  email: z.email(),
  password: z.string().min(8, &quot;Password must be at least 8 characters&quot;),
});

type FormValues = z.infer&amp;lt;typeof formSchema&amp;gt;;

export function LoginForm() {
  const { register, handleSubmit, formState: { errors } } = useForm&amp;lt;FormValues&amp;gt;({
    resolver: zodResolver(formSchema),
  });

  const onSubmit = (data: FormValues) =&amp;gt; console.log(data);

  return (
    &amp;lt;form onSubmit={handleSubmit(onSubmit)}&amp;gt;
      &amp;lt;input {...register(&quot;email&quot;)} /&amp;gt;
      {errors.email &amp;amp;&amp;amp; &amp;lt;p&amp;gt;{errors.email.message}&amp;lt;/p&amp;gt;}

      &amp;lt;input type=&quot;password&quot; {...register(&quot;password&quot;)} /&amp;gt;
      {errors.password &amp;amp;&amp;amp; &amp;lt;p&amp;gt;{errors.password.message}&amp;lt;/p&amp;gt;}

      &amp;lt;button type=&quot;submit&quot;&amp;gt;Log in&amp;lt;/button&amp;gt;
    &amp;lt;/form&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;With Yup:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&quot;use client&quot;;

import { useForm } from &quot;react-hook-form&quot;;
import { yupResolver } from &quot;@hookform/resolvers/yup&quot;;
import * as yup from &quot;yup&quot;;

const formSchema = yup.object({
  email: yup.string().email().required(),
  password: yup.string().min(8, &quot;Password must be at least 8 characters&quot;).required(),
});

type FormValues = yup.InferType&amp;lt;typeof formSchema&amp;gt;;

export function LoginForm() {
  const { register, handleSubmit, formState: { errors } } = useForm&amp;lt;FormValues&amp;gt;({
    resolver: yupResolver(formSchema),
  });

  const onSubmit = (data: FormValues) =&amp;gt; console.log(data);

  return (
    &amp;lt;form onSubmit={handleSubmit(onSubmit)}&amp;gt;
      &amp;lt;input {...register(&quot;email&quot;)} /&amp;gt;
      {errors.email &amp;amp;&amp;amp; &amp;lt;p&amp;gt;{errors.email.message}&amp;lt;/p&amp;gt;}

      &amp;lt;input type=&quot;password&quot; {...register(&quot;password&quot;)} /&amp;gt;
      {errors.password &amp;amp;&amp;amp; &amp;lt;p&amp;gt;{errors.password.message}&amp;lt;/p&amp;gt;}

      &amp;lt;button type=&quot;submit&quot;&amp;gt;Log in&amp;lt;/button&amp;gt;
    &amp;lt;/form&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's different:&lt;/strong&gt; almost nothing at the React Hook Form layer — swap the resolver, swap the schema syntax, and everything else stays identical. This is exactly why the &quot;which one&quot; decision usually comes down to TypeScript ergonomics and team familiarity rather than form-library compatibility, since both are equally well supported.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;async-validation-where-yup-still-has-an-edge&quot;&gt;Async Validation: Where Yup Still Has an Edge&lt;/h3&gt;
&lt;p&gt;If you need to check something against a server mid-validation — &quot;is this username already taken?&quot; — Yup's &lt;code&gt;.test()&lt;/code&gt; method has had mature async support for years:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const signupSchema = yup.object({
  username: yup.string().test(
    &quot;unique-username&quot;,
    &quot;This username is already taken&quot;,
    async (value) =&amp;gt; {
      if (!value) return true;
      const res = await fetch(`/api/check-username?u=${value}`);
      const { available } = await res.json();
      return available;
    }
  ),
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Zod supports async refinements too, using &lt;code&gt;.refine()&lt;/code&gt; with an async function and &lt;code&gt;safeParseAsync()&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const signupSchema = z.object({
  username: z.string(),
}).refine(
  async (data) =&amp;gt; {
    const res = await fetch(`/api/check-username?u=${data.username}`);
    const { available } = await res.json();
    return available;
  },
  { message: &quot;This username is already taken&quot;, path: [&quot;username&quot;] }
);

// must use safeParseAsync, not safeParse, when the schema has async refinements
const result = await signupSchema.safeParseAsync({ username: &quot;ali&quot; });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; both libraries can do this — Yup's API was simply designed around async validation from the start, so it reads slightly more naturally. Zod requires you to remember to swap &lt;code&gt;safeParse&lt;/code&gt; for &lt;code&gt;safeParseAsync&lt;/code&gt; whenever an async &lt;code&gt;.refine()&lt;/code&gt; is in play, which is an easy thing to forget.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Common Mistake:&lt;/strong&gt; Calling &lt;code&gt;safeParse()&lt;/code&gt; (not &lt;code&gt;safeParseAsync()&lt;/code&gt;) on a Zod schema with an async &lt;code&gt;.refine()&lt;/code&gt;. It won't throw — it'll just resolve the refinement incorrectly, since Zod can't await inside a synchronous parse. Always match &lt;code&gt;safeParseAsync&lt;/code&gt; to async refinements.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;migrating-from-yup-to-zod-if-you-re-considering-it-&quot;&gt;Migrating from Yup to Zod (If You're Considering It)&lt;/h3&gt;
&lt;p&gt;If you're weighing a migration, here's the honest advice: &lt;strong&gt;don't rewrite a working Yup codebase just to switch libraries.&lt;/strong&gt; The type-safety gains are real, but they're not worth the regression risk on schemas that already work in production. Reach for Zod on &lt;strong&gt;new&lt;/strong&gt; features or &lt;strong&gt;new&lt;/strong&gt; projects instead, and let old Yup schemas retire naturally as you touch that code for other reasons.&lt;/p&gt;
&lt;p&gt;If you are migrating a specific schema, the field-by-field mapping is usually mechanical:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Yup &lt;/th&gt;&lt;th&gt;Zod&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;yup.string().required()&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;z.string()&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;yup.string().optional()&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;z.string().optional()&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;yup.string().email()&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;z.email()&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;yup.number().integer()&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;z.number().int()&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;yup.object().shape({...})&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;z.object({...})&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;yup.InferType&amp;lt;typeof schema&amp;gt;&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;z.infer&amp;lt;typeof schema&amp;gt;&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3 id=&quot;frequently-asked-questions&quot;&gt;Frequently Asked Questions&lt;/h3&gt;
&lt;h4 id=&quot;can-i-use-zod-without-typescript-&quot;&gt;Can I use Zod without TypeScript?&lt;/h4&gt;
&lt;p&gt;Yes, Zod works fine in plain JavaScript — but you lose its single biggest advantage, since type inference only matters if you have TypeScript to infer &lt;em&gt;into&lt;/em&gt;. If your project is JavaScript-only, Zod and Yup are much closer in practical value, and team familiarity should decide it.&lt;/p&gt;
&lt;h4 id=&quot;is-zod-always-faster-than-yup-&quot;&gt;Is Zod always faster than Yup?&lt;/h4&gt;
&lt;p&gt;Not necessarily, and it depends heavily on the shape of your schema. For most typical form-sized payloads, both validate in well under a millisecond, so raw performance differences almost never matter in practice — TypeScript ergonomics and API fit are the more meaningful factors.&lt;/p&gt;
&lt;h4 id=&quot;do-i-need-to-rewrite-my-formik-forms-to-use-zod-&quot;&gt;Do I need to rewrite my Formik forms to use Zod?&lt;/h4&gt;
&lt;p&gt;No. Formik and Yup are still a perfectly fine, actively maintained combination for existing codebases. Only reach for a migration if you're already refactoring that form layer for other reasons.&lt;/p&gt;
&lt;h4 id=&quot;which-one-works-better-with-trpc-&quot;&gt;Which one works better with tRPC?&lt;/h4&gt;
&lt;p&gt;Zod, by a wide margin — tRPC's input/output validation is built with Zod's type inference in mind, and most tRPC examples and starter templates assume Zod. Yup can technically be adapted, but it's fighting the grain.&lt;/p&gt;
&lt;h4 id=&quot;can-i-use-both-zod-and-yup-in-the-same-project-&quot;&gt;Can I use both Zod and Yup in the same project?&lt;/h4&gt;
&lt;p&gt;Technically yes, but it's not something to do on purpose. If you're migrating incrementally, it's fine to have both installed temporarily — just avoid mixing them within the same feature or form, since their default behaviors (required vs. optional fields, sync vs. async) differ enough to cause confusing bugs.&lt;/p&gt;
&lt;h4 id=&quot;does-zod-support-transforming-data-not-just-validating-it-&quot;&gt;Does Zod support transforming data, not just validating it?&lt;/h4&gt;
&lt;p&gt;Yes — &lt;code&gt;.transform()&lt;/code&gt; lets you reshape data as part of validation, like converting a string date into a &lt;code&gt;Date&lt;/code&gt; object. Yup supports this too, through &lt;code&gt;.transform()&lt;/code&gt; on its schemas. Both are equally capable here.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;wrapping-up&quot;&gt;Wrapping Up&lt;/h3&gt;
&lt;p&gt;Zod and Yup solve the exact same problem — turning &quot;hope the data is right&quot; into &quot;know the data is right&quot; — but they were built for different eras of the JavaScript ecosystem. Zod's schema-is-the-type approach is what modern TypeScript tooling expects in 2026, and it's the right default for anything new you're building. Yup remains a completely legitimate choice if you're maintaining an existing Formik-based app, and there's no urgency to rip it out just because a newer library exists.&lt;/p&gt;
&lt;p&gt;If you're starting a project today: reach for Zod, pair it with &lt;code&gt;@hookform/resolvers/zod&lt;/code&gt; for your forms, and use &lt;code&gt;.safeParse()&lt;/code&gt; on the server inside every Server Action or API route that touches user input — never trust that client-side validation alone is enough.&lt;/p&gt;
&lt;h3 id=&quot;try-this-free-developer-tool&quot;&gt;Try This Free Developer Tool&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/tools/zod-schema-generator&quot;&gt;Zod Schema Generator&lt;/a&gt; — paste a JSON sample or a TypeScript interface and get a production-ready Zod schema instantly, complete with smart field-name inference, a live validator, and ready-to-use React Hook Form, API Route, and Server Action output.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;continue-learning&quot;&gt;Continue Learning&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/role-based-authentication-nextjs-16&quot;&gt;Role-Based Access Control (RBAC) in Next.js 16 (2026 Beginner's Guide)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/nextjs-16-route-handlers&quot;&gt;Route Handlers Explained in Next.js 16 (Beginner's Guide)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/nextjs-16-supabase-todo-app&quot;&gt;Build a Todo App with Next.js 16 and Supabase (2026 Guide)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/25116/zod-vs-yup-in-2026-which-validation-library-should-you-use</guid>
<pubDate>Sat, 22 Aug 2026 04:00:34 +0000</pubDate>
</item>
<item>
<title>Building Type-Safe AI Prompts with TypeScript</title>
<link>https://coderlegion.com/23130/building-type-safe-ai-prompts-with-typescript</link>
<description>&lt;h3 id=&quot;building-type-safe-ai-prompts-with-typescript&quot;&gt;Building Type-Safe AI Prompts with TypeScript&lt;/h3&gt;
&lt;p&gt;In the previous article, we explored why prompt strings become difficult to manage as AI applications grow.&lt;/p&gt;
&lt;p&gt;But identifying the problem is only half the journey.&lt;/p&gt;
&lt;p&gt;The next question is:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;How should we build prompts instead?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;As developers, we already have great tools for building reliable software.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;TypeScript catches type errors.&lt;/li&gt;
&lt;li&gt;Zod validates runtime data.&lt;/li&gt;
&lt;li&gt;ESLint catches mistakes before production.&lt;/li&gt;
&lt;li&gt;Unit tests prevent regressions.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So why are prompts still written as plain strings?&lt;/p&gt;
&lt;p&gt;Today we'll explore how &lt;strong&gt;type-safe prompt engineering&lt;/strong&gt; makes AI applications more reliable and easier to maintain.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;traditional-prompt-engineering&quot;&gt;Traditional Prompt Engineering&lt;/h2&gt;
&lt;p&gt;Most applications still look something like this.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const prompt = `
You are an expert technical writer.

Summarize the following article.

Language: ${language}

Tone: ${tone}

Article:

${article}
`;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Looks simple.&lt;/p&gt;
&lt;p&gt;But what happens if someone writes&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;generatePrompt({
  language: &quot;English&quot;
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Oops.&lt;/p&gt;
&lt;p&gt;They forgot&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;article
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;TypeScript doesn't complain.&lt;/p&gt;
&lt;p&gt;Your IDE doesn't complain.&lt;/p&gt;
&lt;p&gt;The application compiles successfully.&lt;/p&gt;
&lt;p&gt;You only discover the issue after making an expensive API request.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-problem-isn-t-ai&quot;&gt;The Problem Isn't AI&lt;/h2&gt;
&lt;p&gt;The problem is that &lt;strong&gt;strings don't describe structure&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Consider this prompt.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;`
Summarize

${text}

Audience

${audience}

Output

${format}
`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Questions immediately arise.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Is &lt;code&gt;text&lt;/code&gt; required?&lt;/li&gt;
&lt;li&gt;Can &lt;code&gt;audience&lt;/code&gt; be empty?&lt;/li&gt;
&lt;li&gt;What values are allowed?&lt;/li&gt;
&lt;li&gt;What format should the output follow?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The string itself can't answer any of these questions.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;introducing-structure&quot;&gt;Introducing Structure&lt;/h2&gt;
&lt;p&gt;Instead of describing prompts as text...&lt;/p&gt;
&lt;p&gt;Describe them as &lt;strong&gt;objects&lt;/strong&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const summarize = pf.define({

  input: z.object({

    article: z.string(),

    audience: z.enum([
      &quot;developer&quot;,
      &quot;student&quot;,
      &quot;executive&quot;
    ])

  }),

  output: z.object({

    summary: z.string()

  }),

  messages: ({ article, audience }) =&amp;gt; [

    pf.system`
      You are an expert writer.

      Tailor explanations for a ${audience}.
    `,

    pf.user`
      Summarize:

      ${article}
    `

  ]

});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now your prompt has an actual API.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;type-inference-for-free&quot;&gt;Type Inference for Free&lt;/h2&gt;
&lt;p&gt;One of my favorite parts of TypeScript is that you rarely need to write interfaces manually.&lt;/p&gt;
&lt;p&gt;The same idea applies here.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;type Input = pf.inferInput&amp;lt;typeof summarize&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Immediately becomes&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;{
  article: string;
  audience: &quot;developer&quot; | &quot;student&quot; | &quot;executive&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No duplicated types.&lt;/p&gt;
&lt;p&gt;No maintenance.&lt;/p&gt;
&lt;p&gt;Everything stays synchronized automatically.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;runtime-validation&quot;&gt;Runtime Validation&lt;/h2&gt;
&lt;p&gt;TypeScript only protects you during development.&lt;/p&gt;
&lt;p&gt;What happens when data comes from an API?&lt;/p&gt;
&lt;p&gt;Or a form?&lt;/p&gt;
&lt;p&gt;Or user input?&lt;/p&gt;
&lt;p&gt;That's where runtime validation matters.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;summarize.compile({

  article: 42,

  audience: &quot;developer&quot;

});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Instead of silently failing later...&lt;/p&gt;
&lt;p&gt;PromptForge immediately throws&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;PromptValidationError

Expected

article: string

Received

number
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You catch mistakes before calling the LLM.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;strongly-typed-outputs&quot;&gt;Strongly Typed Outputs&lt;/h2&gt;
&lt;p&gt;Inputs aren't the only thing that benefit from schemas.&lt;/p&gt;
&lt;p&gt;Outputs do too.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;output: z.object({

  title: z.string(),

  summary: z.string(),

  keywords: z.array(z.string())

})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now your application knows exactly what shape the response should have.&lt;/p&gt;
&lt;p&gt;This opens the door to&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Structured Outputs&lt;/li&gt;
&lt;li&gt;Tool Calling&lt;/li&gt;
&lt;li&gt;JSON validation&lt;/li&gt;
&lt;li&gt;Better autocomplete&lt;/li&gt;
&lt;li&gt;Safer parsing&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;reusable-prompt-components&quot;&gt;Reusable Prompt Components&lt;/h2&gt;
&lt;p&gt;Most AI applications repeat instructions.&lt;/p&gt;
&lt;p&gt;Instead of copying them...&lt;/p&gt;
&lt;p&gt;Create reusable blocks.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const safety = pf.define({

  messages: () =&amp;gt; [

    pf.system`
      Never expose secrets.
    `

  ]

});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then compose them.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const assistant = pf.define({

  input: z.object({

    question: z.string()

  }),

  messages: ({ question }) =&amp;gt; [

    pf.include(safety),

    pf.user`${question}`

  ]

});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No duplicated strings.&lt;/p&gt;
&lt;p&gt;No copy-paste.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;better-developer-experience&quot;&gt;Better Developer Experience&lt;/h2&gt;
&lt;p&gt;Because PromptForge understands your schema...&lt;/p&gt;
&lt;p&gt;Your editor can help you.&lt;/p&gt;
&lt;p&gt;When typing&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;assistant.compile({
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;VS Code immediately suggests&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;question
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Autocomplete.&lt;/p&gt;
&lt;p&gt;Hover information.&lt;/p&gt;
&lt;p&gt;Type checking.&lt;/p&gt;
&lt;p&gt;Everything developers already expect from modern tooling.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;why-this-matters&quot;&gt;Why This Matters&lt;/h2&gt;
&lt;p&gt;Large AI applications eventually become collections of prompts.&lt;/p&gt;
&lt;p&gt;Those prompts deserve the same engineering principles as the rest of your codebase.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Modular&lt;/li&gt;
&lt;li&gt;Reusable&lt;/li&gt;
&lt;li&gt;Type-safe&lt;/li&gt;
&lt;li&gt;Testable&lt;/li&gt;
&lt;li&gt;Validated&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Treating prompts like software makes them dramatically easier to maintain.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;looking-ahead&quot;&gt;Looking Ahead&lt;/h2&gt;
&lt;p&gt;Type safety is only the beginning.&lt;/p&gt;
&lt;p&gt;In the next article, we'll explore one of the most powerful ideas behind PromptForge:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Composable Prompt Engineering&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Instead of copying prompts across your project, we'll build reusable prompt blocks that can be combined just like React components.&lt;/p&gt;
&lt;p&gt;Once you start composing prompts, you'll never want to go back to copy-pasting instructions.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;installation&quot;&gt;Installation&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install @promptforgee/core
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id=&quot;resources&quot;&gt;Resources&lt;/h2&gt;
&lt;p&gt; Documentation&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://prompt-forge-docs.vercel.app/&quot;&gt;https://prompt-forge-docs.vercel.app/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;⭐ GitHub&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Omnikon-Org/PromptForge&quot;&gt;https://github.com/Omnikon-Org/PromptForge&lt;/a&gt;&lt;/p&gt;
&lt;p&gt; npm&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/@promptforgee/core&quot;&gt;https://www.npmjs.com/package/@promptforgee/core&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;final-thoughts&quot;&gt;Final Thoughts&lt;/h2&gt;
&lt;p&gt;Prompt engineering is quickly becoming an essential part of modern software development.&lt;/p&gt;
&lt;p&gt;The better our applications become, the more valuable our prompts become.&lt;/p&gt;
&lt;p&gt;So maybe it's time we stop treating prompts like strings...&lt;/p&gt;
&lt;p&gt;...and start treating them like software.&lt;/p&gt;
&lt;p&gt;If you're building AI applications with TypeScript, I'd love to hear your thoughts.&lt;/p&gt;
&lt;p&gt;How are you currently managing prompts in your projects?&lt;br&gt;
&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/57aq7qbkx7y8b9f972bf.png&quot; alt=&quot;Image description&quot;&gt;&lt;br&gt;
&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/gzncaeljq8w98b5uqr7q.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/23130/building-type-safe-ai-prompts-with-typescript</guid>
<pubDate>Tue, 21 Jul 2026 09:58:49 +0000</pubDate>
</item>
<item>
<title>I got tired of faker mocks silently breaking my tests, so I built zodmint</title>
<link>https://coderlegion.com/18930/i-got-tired-of-faker-mocks-silently-breaking-my-tests-so-i-built-zodmint</link>
<description>&lt;p&gt;You know the pattern: you write a factory with faker, tests pass locally, then CI blows up because &lt;code&gt;faker.number.int()&lt;/code&gt; generated a negative value that fails your &lt;code&gt;.positive()&lt;/code&gt; constraint. The mock was lying the whole time.&lt;/p&gt;
&lt;p&gt;I built zodmint to fix this. You pass a Zod schema in, you get a valid value out, guaranteed to pass &lt;code&gt;schema.safeParse(output).success === true&lt;/code&gt;. No faker, no hand-written factories.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install zodmint
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Basic usage:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const UserSchema = z.object({
  id: z.uuid(),
  email: z.string().email(),
  age: z.number().int().min(18).max(99),
  role: z.enum([&quot;admin&quot;, &quot;user&quot;, &quot;guest&quot;]),
});

mock(UserSchema);
// { id: 'b48653da-...', email: '*Emails are not allowed*', age: 41, role: 'admin' }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It also has:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;mockFactory()&lt;/code&gt; with named states, &lt;code&gt;afterBuild&lt;/code&gt; hook, and &lt;code&gt;extend()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Seeded generation (same seed = same output, great for snapshots)&lt;/li&gt;
&lt;li&gt;Edge mode (boundary values for stress testing)&lt;/li&gt;
&lt;li&gt;Custom matchers and a plugin system for domain-specific values&lt;/li&gt;
&lt;li&gt;Full support for &lt;code&gt;z.union&lt;/code&gt;, &lt;code&gt;z.discriminatedUnion&lt;/code&gt;, &lt;code&gt;z.intersection&lt;/code&gt;, &lt;code&gt;z.lazy&lt;/code&gt;, &lt;code&gt;z.refine&lt;/code&gt;, and more&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Got some downloads in the first 24h without posting anywhere which surprised me. Would love feedback from people actually using it.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;npm: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/zodmint&quot;&gt;https://www.npmjs.com/package/zodmint&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/gonll/zodmint&quot;&gt;https://github.com/gonll/zodmint&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/18930/i-got-tired-of-faker-mocks-silently-breaking-my-tests-so-i-built-zodmint</guid>
<pubDate>Mon, 25 May 2026 22:44:32 +0000</pubDate>
</item>
<item>
<title>Get Started with React Hook Form</title>
<link>https://coderlegion.com/7429/get-started-with-react-hook-form</link>
<description>&lt;h3 id=&quot;introduction&quot;&gt;Introduction&lt;/h3&gt;
&lt;p&gt;Hi, guys!&lt;/p&gt;
&lt;p&gt;Recently, I spent some time improving my React skills. And the feeling that I will never know enough never goes away, but that is another talk. Just know it is perfectly normal to feel this way.&lt;/p&gt;
&lt;p&gt;This is how I found out about React Hook Form. Ok, actually, it wasn't first time I heard about it, but now I just decided to learn more about it. So I just read their docs a little bit and decided I will share the knowledge with you. &lt;/p&gt;
&lt;p&gt;I won't dive too deep into the subject. The goal is to get as familiar as possible with this library.&lt;/p&gt;
&lt;p&gt;So let's get started!&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-is-react-hook-form-and-why-should-you-use-it-&quot;&gt;What is React Hook Form and Why Should You Use It?&lt;/h3&gt;
&lt;p&gt;It is a light library that helps you manage forms easily in React.&lt;/p&gt;
&lt;p&gt;What it offers:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Light&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Provides an intuitive experience, with rich features, all of these while staying light and not relying on other dependencies.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Performance&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;It reduces the amount of code, it reduces the number of re-renders, due to its use of uncontrolled components by default and offers faster mounting and reduced validation computations. Because the form components don't update the state with every keystroke, the components and its children don't re-render.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Validations&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;React Hook Form leverages existing HTML markup and makes form validation easy.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Error Handling&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;It provides an &lt;code&gt;errors object&lt;/code&gt; which you can use to retrieve errors.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Loading State&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Methods like &lt;code&gt;isSubmitting&lt;/code&gt;, &lt;code&gt;isLoading&lt;/code&gt;, &lt;code&gt;isValidating&lt;/code&gt; makes managing loading state more intuitive.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;installation-and-basic-usage&quot;&gt;Installation and Basic Usage&lt;/h3&gt;
&lt;p&gt;React Hook Form is easy to setup. Start by using the following command in your terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install react-hook-form
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Imagine how you do it normally without React Hook Form:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import { useState } from 'react';

const Form = () =&amp;gt; {
  const [ name, setName ] = useState('');

  return (
    &amp;lt;form&amp;gt;
      &amp;lt;input onChange={() =&amp;gt; setName(e.target.value)} value={name} /&amp;gt;
    &amp;lt;/form&amp;gt;
  );
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is fine, right? It is a very small form, won't create any problems, hopefully. But what if it scales? What if instead of one input you get 10? Imagine you have to manage that huge form state, together with loading and error handling. And think about performance and the amount of code written. The number of re-renders will be huge, maybe the app will be sluggish. &lt;/p&gt;
&lt;p&gt;And this is how it comes to the rescue. Let's demonstrate some basic usage:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import { useForm } from &quot;react-hook-form&quot;;

export default function App() {
  const { register, handleSubmit } = useForm();
  const onSubmit = (data) =&amp;gt; console.log(data);

  return (
    /* &quot;handleSubmit&quot; will validate inputs before invoking &quot;onSubmit&quot; */
    &amp;lt;form onSubmit={handleSubmit(onSubmit)}&amp;gt;
     {/* register your input into the hook by invoking the &quot;register&quot; function */}
     &amp;lt;input {...register(&quot;example&quot;)} /&amp;gt;
     &amp;lt;input type=&quot;submit&quot; /&amp;gt;
    &amp;lt;/form&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When you call &lt;code&gt;useForm()&lt;/code&gt;, the function returns a number of useful functions. We will talk about some of them in the next sections, but two of the most important ones are &lt;code&gt;register&lt;/code&gt; and &lt;code&gt;handleSubmit&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;register&lt;/strong&gt; function, as the name implies, will register your input into the hook and will apply validation rules to it. What does it mean? It means you now don't need &lt;code&gt;useState&lt;/code&gt; and &lt;code&gt;onChange&lt;/code&gt; and &lt;code&gt;value&lt;/code&gt; to manage your input state. It is managed by React Hook Form, but leveraging uncontrolled components.&lt;/p&gt;
&lt;p&gt;When you actually use it inside an input, the syntax is this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;&amp;lt;input {...register(&quot;name&quot;), validations} /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It accepts two parameters. The first one will be a string and it's going to be a name of your choosing. Keep in mind that this is like applying a &lt;code&gt;name attribute&lt;/code&gt; on a HTML tag and you are going to use this name with other useForm() functions as well. The second attribute is an object with validations, which we'll discuss in the next section. So you could have something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import { useForm } from &quot;react-hook-form&quot;;

export default function App() {
  const { register, handleSubmit } = useForm();
  const onSubmit = (data) =&amp;gt; console.log(data);

  return (
    &amp;lt;form onSubmit={handleSubmit(onSubmit)}&amp;gt;
     &amp;lt;input {...register(&quot;firstName&quot;, { reguired: true, maxLength: 20 })} /&amp;gt;
     &amp;lt;input type=&quot;submit&quot; /&amp;gt;
    &amp;lt;/form&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;handleSubmit&lt;/strong&gt; will receive your form data only if input validation ended successfuly. It accepts two callbacks, one for success and one for errors.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import { useForm } from &quot;react-hook-form&quot;;

export default function App() {
  const { register, handleSubmit } = useForm();
  const onSubmit = (data) =&amp;gt; console.log(data);
  const onError = (errors) =&amp;gt; console.log(errors);

  return (
    &amp;lt;form onSubmit={handleSubmit(onSubmit, onError)}&amp;gt;
      &amp;lt;input {...register(&quot;firstName&quot;)} /&amp;gt;
      &amp;lt;input {...register(&quot;lastName&quot;)} /&amp;gt;
      &amp;lt;input type=&quot;email&quot; {...register(&quot;email&quot;)} /&amp;gt;
      &amp;lt;input type=&quot;submit&quot; /&amp;gt;
    &amp;lt;/form&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;validations-error-handling-and-loading&quot;&gt;Validations, Error Handling and Loading&lt;/h3&gt;
&lt;p&gt;React Hook Form aligns with HTML standard for form validations. This means it supports rules like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;required&lt;/li&gt;
&lt;li&gt;min&lt;/li&gt;
&lt;li&gt;max&lt;/li&gt;
&lt;li&gt;minLength&lt;/li&gt;
&lt;li&gt;maxLength&lt;/li&gt;
&lt;li&gt;pattern&lt;/li&gt;
&lt;li&gt;validate&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Let's see how it works:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import { useForm } from &quot;react-hook-form&quot;;

export default function App() {
  const { register, handleSubmit } = useForm();
  const onSubmit = (data) =&amp;gt; console.log(data);

  return (
    &amp;lt;form onSubmit={handleSubmit(onSubmit)}&amp;gt;
     &amp;lt;input {...register(&quot;firstName&quot;, { reguired: true, maxLength: 20, minLength: 1 })} /&amp;gt;
     &amp;lt;input type=&quot;number&quot; {...register(&quot;age&quot;, { required: true, min: 18, max: 99 })} /&amp;gt;
     &amp;lt;input type=&quot;number&quot; {...register(&quot;experience&quot;), { pattern: /\d/ }}
     &amp;lt;input type=&quot;number&quot; {...register(&quot;lastName&quot;), {
       validate: { (lastName) =&amp;gt; if (!lastName) =&amp;gt; 'Last name is required' }
     }} 
     &amp;lt;input type=&quot;submit&quot; /&amp;gt;
    &amp;lt;/form&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;While some of them are naturally logical and intuitive, the &lt;code&gt;validate&lt;/code&gt; rule asks for some explanations. It is an object where you can insert as many validation methods (functions) as you need. For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;register('fieldName', {
  validate: {
    // 1. First Validation Function
    mustBeAdult: (value) =&amp;gt; value &amp;gt;= 18 || &quot;Must be 18 or older&quot;,
    // 2. Second Validation Function
    isEvenNumber: (value) =&amp;gt; value % 2 === 0 || &quot;Must be an even number&quot;,
    // 3. Third Validation Function
    notZero: (value) =&amp;gt; value !== 0 || &quot;Cannot be zero&quot;,
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;How does error handling work with validate rule? The validation functions run in the order they are defined and if a validation function returns true, validation passes for that rule.&lt;/p&gt;
&lt;p&gt;If a function returns a string like &quot;Must be 18 or older&quot;, that string is treated as the error message, and validation stops for that field immediately and will display the error message from the first function that fails.&lt;/p&gt;
&lt;p&gt;There are other rules for validations inside register function as well. Read more about them &lt;a rel=&quot;nofollow&quot; href=&quot;https://react-hook-form.com/docs/useform/register&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;To &lt;strong&gt;handle errors&lt;/strong&gt; in React Hook Form is easy. You first have to destructure the useForm() hook and get the &lt;code&gt;formState&lt;/code&gt;, which you will then destructure to get &lt;code&gt;errors&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import { useForm } from 'react-hook-form';

const Form = () =&amp;gt; {
  const { register, handleSubmit, formState: {errors} } = useForm();

 return (
    &amp;lt;form onSubmit={handleSubmit(onSubmit)}&amp;gt;
     &amp;lt;input {...register(&quot;firstName&quot;, { reguired: 'Name required', maxLength: { value: 20, message: 'No more than 20' }} )} /&amp;gt;
     {errors.firstName.message &amp;amp;&amp;amp; &amp;lt;p&amp;gt;{errors}&amp;lt;/p&amp;gt;}
     &amp;lt;input type=&quot;number&quot; {...register(&quot;age&quot;, { required: true, min: { value: 18, message: 'At least 18' }, max: 99 })} /&amp;gt;
     {errors.age.message &amp;amp;&amp;amp; &amp;lt;p&amp;gt;{errors}&amp;lt;/p&amp;gt;}
     &amp;lt;input type=&quot;submit&quot; /&amp;gt;
    &amp;lt;/form&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The syntax for writing errors is something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;{...register(&quot;age&quot;, { required: true, min: { value: 18, message: 'At least 18' }})}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And instead of &lt;code&gt;required: true&lt;/code&gt;, you can also give this property a string value, which will act as an error message. Now if the field is not filled you will see 'Name required' under it. Notice that when you register your input, the errors object will get a key that will match the name you registered the input with. And that key will be an object which will hold the 'message' property. That is how you can smartly access the errors for every field. No headache required. So in our case:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;  errors.firstName.message
  errors.age.message
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The formState also holds other useful properties. Check them all &lt;a rel=&quot;nofollow&quot; href=&quot;https://react-hook-form.com/docs/useform/formstate&quot;&gt;here&lt;/a&gt;. We'll also discuss here some of them that are related to loading state: &lt;strong&gt;isSubmitting&lt;/strong&gt;, &lt;strong&gt;isLoading&lt;/strong&gt;, &lt;strong&gt;isValidating&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;All of these three are booleans. And you guessed right, isSubmitting is true during the time the form is submitting. You might use it when you want to disable a submit button during form submission. isLoading only works with default async values and it is true when the form is loading these values. Example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const { formState: { isLoading, isSubmitting } } = useForm({
  defaultValues: async () =&amp;gt; await fetch('/api');
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We didn't say anything about default values in useForm as props yet. But it is easy to understand. It's like using defaultValue in React. However, as the documentation recommends:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;The defaultValues prop populates the entire form with default values. It supports both synchronous and asynchronous assignment of default values. While you can set an input's default value using defaultValue or defaultChecked (as detailed in the official React documentation), it is recommended to use defaultValues for the entire form.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;So you will use isLoading if you have default values that are async, in other words, if those values, that you want as defaults for your inputs, take time until they are loaded into your form.&lt;/p&gt;
&lt;p&gt;The last one, isValidating is going to be true during validation, otherwise false. Example for all:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import { useForm } from &quot;react-hook-form&quot;;
import { useState } from 'react';

export default function App() {
  const [ value, setValue ] = useState();
  const { register, handleSubmit, formState: {errors, isSubmitting, isLoading, isValidating},
  } = useForm({
    defaultValues: async () =&amp;gt; await someValue().then((value) =&amp;gt; setValue(value)); 
  });
  const onSubmit = (data) =&amp;gt; console.log(data);

  return (
    &amp;lt;form onSubmit={handleSubmit(onSubmit)}&amp;gt;
       {isValidating? &amp;lt;p&amp;gt;Validating your form...&amp;lt;/p&amp;gt; : &amp;lt;p&amp;gt;Validated successfuly!&amp;lt;/p&amp;gt;}
      {isLoading? &amp;lt;p&amp;gt;The value is loading&amp;lt;/p&amp;gt; : &amp;lt;p&amp;gt;{value}&amp;lt;/p&amp;gt;}
      &amp;lt;input {...register(&quot;test&quot;)} /&amp;gt;
      &amp;lt;button disabled={isSubmitting? true : false}&amp;gt;{isSubmitting? 'Submitting...' : 'Submit'}&amp;lt;/button&amp;gt;
    &amp;lt;/form&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;useform-functions-and-props&quot;&gt;UseForm Functions and Props&lt;/h3&gt;
&lt;p&gt;When you call &lt;strong&gt;useForm()&lt;/strong&gt;, you make available a useful set of properties and methods. Some of them we already discussed, like register, formState, handleSubmit. Some others are &lt;code&gt;unregister&lt;/code&gt;, &lt;code&gt;setErrors&lt;/code&gt;, &lt;code&gt;watch&lt;/code&gt;, &lt;code&gt;setValue&lt;/code&gt;, &lt;code&gt;reset&lt;/code&gt;, &lt;code&gt;setFocus&lt;/code&gt;. And there are other more. &lt;/p&gt;
&lt;p&gt;Note that all these returned functions from useForm have some other options as their second parameters. Chech the &lt;a rel=&quot;nofollow&quot; href=&quot;https://react-hook-form.com/docs/useform&quot;&gt;documentation&lt;/a&gt; for more information.&lt;/p&gt;
&lt;p&gt;Now let's say a thing or two about these form above for now, just to get the hang of it.&lt;/p&gt;
&lt;p&gt;You will want to use &lt;strong&gt;unregister&lt;/strong&gt; when you need to programmatically remove a field's data and validation rules from the form state. Imagine you have some fields in your app that are conditionally rendered. Imagine a field that is not rendered if a checkbox is not checked, being conditionally rendered. Without unregistering this field, even if the field is not there visually, it exists as state and validation. When you submit the form, the form is also submitted with data from this field, that you didn't even use. You can expect some problems in your app if you don't unregister.&lt;/p&gt;
&lt;p&gt;You can do this programatically using useEffect. This is a cleanup that will run whenever the component will unmount. So this how you remove the field's state and data.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const { unregister } = useForm();

useEffect(() =&amp;gt; {
  return () =&amp;gt; {
    unregister(&quot;fieldName&quot;);
  };
}, [unregister]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;However, there is another approach, a more modern one, that implies enabling &lt;strong&gt;automatic unregistration&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;Just like using defaultValues prop, this is another prop in useForm that clean up the field state when the component unmounts and makes your code much cleaner. Note that this is false by default.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const methods = useForm({
  // Set this option to true
  shouldUnregister: true, 
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;strong&gt;setErrors&lt;/strong&gt; and &lt;strong&gt;setValue&lt;/strong&gt; are both valuable when you need to programatically set other errors that are maybe not validation related, such as network errors or so and when you want to set a value, when clicking a button, for example. &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const { unregister } = useForm();

// pretend it's a network error
if (networkError) {
  setError('name of the field', { type: string, message: 'Network error'});
}

// pretend you clicked a button to change an input value 
if (buttonClicked) {
  setValue('name of the field', value);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;strong&gt;reset&lt;/strong&gt; function allows you to reset the whole form state. It accepts options which allow for partial reset. &lt;em&gt;setFocus&lt;/em&gt; allow to set focus on a field programatically. It accept a string value, which is going to be the name of the field you want to focus on and some options as an object.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;watch&lt;/strong&gt; function lets you subscribe to input changes. Keep in mind that this will activate re-rendering. You can watch a sinlge field, multiple fields of your choice or the entire form.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// single field
const form = watch();

// multiple fields
const names = watch([ 'firstName', 'lastName' ]);

// entire form
const name = watch(&quot;name&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;strong&gt;useForm()&lt;/strong&gt; accepts props. We have already seen some of them earlier, like &lt;strong&gt;defaultValues&lt;/strong&gt;, &lt;strong&gt;shouldUnsubscribe&lt;/strong&gt;. But there are many others: disabled, errors, values. And also there some specific ones that are related to validation libraries, like zod and yup, which are called &lt;strong&gt;resolvers&lt;/strong&gt;. You can read more about them &lt;a rel=&quot;nofollow&quot; href=&quot;https://react-hook-form.com/docs/useform&quot;&gt;here&lt;/a&gt;. &lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;implementation-with-zod-typescript&quot;&gt;Implementation with Zod + TypeScript&lt;/h3&gt;
&lt;p&gt;We have seen that React Hook Form also accepts validations by default. If you want to go even further, you can make use of Zod and TypeScript. These are modern ways of validating user inputs and make your app more robust.&lt;/p&gt;
&lt;p&gt;You can create a Zod schema and apply it to the hook. This is how:&lt;br&gt;
First you have to install the resolvers.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm i @hookform/resolvers
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then you import it, create the schema and use it in the props of &lt;strong&gt;useForm()&lt;/strong&gt; hook;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import { zodResolver } from '@hookform/resolvers/zod;

interface schema {
  firstName: string,
  lastName: string
}

const { register, handleSubmit } = useForm&amp;lt;schema&amp;gt;({
  resolver: zodResolver(schema)
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will validate your input against the schema and return with either errors or a valid result.&lt;/p&gt;
&lt;p&gt;React Hook Form doesn't only support Zod. It also supports schema-based form validation with Yup, Superstruct or Joi.&lt;/p&gt;
&lt;h3 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;I hope I made React Form Hook at least a bit more easier to start with. Now you've got the basics down... Next step is to go a little deeper according to your needs.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://react-hook-form.com/get-started&quot;&gt;&lt;strong&gt;Check out the official documentation&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;As for more mature developers, what do you think about React Hook Form?&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;connect-with-me&quot;&gt;Connect with me&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;I am open for work, for collaborations, for freelancing or just tech related discussions.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;So for anything, just let me know, leave me a message and I'll get in touch as soon as possible!&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Follow me and see my work on &lt;strong&gt;Github&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/alexandru-ene-dev&quot;&gt;alexandru-ene-dev&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Let's be friends on: &lt;strong&gt;LinkedIn&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.linkedin.com/in/alexandru-ene-dev&quot;&gt;alexandru-ene-dev&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Let's chat on &lt;strong&gt;Discord&lt;/strong&gt;: alexandru.ene.dev&lt;/li&gt;
&lt;li&gt;Find me also on &lt;strong&gt;Dev.to&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/alexandru-ene-dev&quot;&gt;alexandru-ene-dev&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Please, do tell me if you have any sugestions for new content or how to improve it. Your feedback is important to me!&lt;/p&gt;
&lt;p&gt;Thank you so much for reading and I will see you in the next one soon! Happy coding! :)&lt;/p&gt;
</description>
<category>Java</category>
<guid isPermaLink="true">https://coderlegion.com/7429/get-started-with-react-hook-form</guid>
<pubDate>Mon, 17 Nov 2025 11:22:30 +0000</pubDate>
</item>
</channel>
</rss>