<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ zod - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ zod - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Tue, 25 Aug 2026 13:27:57 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/zod/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Build Type-Safe APIs with Hono and Zod ]]>
                </title>
                <description>
                    <![CDATA[ If you've shipped a Node.js API before, you already know this pain: your TypeScript types say one thing, your runtime validation says another, and your OpenAPI docs quietly disagree with both. Someone ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-type-safe-apis-with-hono-and-zod/</link>
                <guid isPermaLink="false">6a8c4f18dbeb4eb3a4b2ed0d</guid>
                
                    <category>
                        <![CDATA[ TypeScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ hono ]]>
                    </category>
                
                    <category>
                        <![CDATA[ zod ]]>
                    </category>
                
                    <category>
                        <![CDATA[ api ]]>
                    </category>
                
                    <category>
                        <![CDATA[ backend ]]>
                    </category>
                
                    <category>
                        <![CDATA[ webdev ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Chinedu Otutu ]]>
                </dc:creator>
                <pubDate>Mon, 24 Aug 2026 14:03:04 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/ce251811-43ad-4e2f-9761-83b6240f718e.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>If you've shipped a Node.js API before, you already know this pain: your TypeScript types say one thing, your runtime validation says another, and your OpenAPI docs quietly disagree with both.</p>
<p>Someone adds a field to an interface, and the schema never gets updated. The docs stay stale until a client files a bug. There's no compiler error or failing test. The three sources of truth just drift apart.</p>
<p>In this tutorial, you'll learn how to collapse those three concerns into one definition using <a href="https://hono.dev">Hono</a> and <a href="https://zod.dev">Zod</a>. You'll build the same patterns I use in production, including in <a href="https://github.com/otutukingsley/clipforge">ClipForge</a>, an open-source video processing toolkit I built and maintain. They help validation, types, and docs stay in sync by design.</p>
<p>By the end of this article, you should know how to:</p>
<ul>
<li><p>Define one Zod schema that handles runtime validation, TypeScript types, and OpenAPI docs</p>
</li>
<li><p>Structure routes so the contract and the handler stay separated</p>
</li>
<li><p>Keep database schemas and HTTP schemas as two deliberate layers</p>
</li>
<li><p>Return one consistent error shape from every failure path</p>
</li>
<li><p>Carry those same patterns from a small Tasks API into a real multi-service system</p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To get the most out of this article, you'll need to know:</p>
<ul>
<li><p>JavaScript and basic TypeScript</p>
</li>
<li><p>How REST APIs work (routes, request bodies, status codes)</p>
</li>
<li><p>A little Node.js (installing packages, running scripts)</p>
</li>
</ul>
<p>You don't need prior experience with Hono, Zod, or Drizzle.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-1-the-drift-problem">1. The Drift Problem</a></p>
</li>
<li><p><a href="#heading-2-what-is-hono">2. What Is Hono?</a></p>
</li>
<li><p><a href="#heading-3-what-is-zod">3. What Is Zod?</a></p>
</li>
<li><p><a href="#heading-4-one-schema-three-jobs">4. One Schema, Three Jobs</a></p>
</li>
<li><p><a href="#heading-5-how-to-set-up-the-project">5. How to Set Up the Project</a></p>
</li>
<li><p><a href="#heading-6-how-to-define-your-api-schemas">6. How to Define Your API Schemas</a></p>
</li>
<li><p><a href="#heading-7-how-to-separate-database-schemas-from-api-schemas">7. How to Separate Database Schemas from API Schemas</a></p>
</li>
<li><p><a href="#heading-8-how-to-define-routes-as-contracts">8. How to Define Routes as Contracts</a></p>
</li>
<li><p><a href="#heading-9-how-to-keep-handlers-thin">9. How to Keep Handlers Thin</a></p>
</li>
<li><p><a href="#heading-10-how-to-return-one-error-shape-everywhere">10. How to Return One Error Shape Everywhere</a></p>
</li>
<li><p><a href="#heading-11-how-to-generate-docs-that-cant-drift">11. How to Generate Docs That Can't Drift</a></p>
</li>
<li><p><a href="#heading-12-how-to-make-the-app-production-ready">12. How to Make the App Production-Ready</a></p>
</li>
<li><p><a href="#heading-13-how-these-patterns-scale-in-a-production-app">13. How These Patterns Scale in a Production App</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-1-the-drift-problem">1. The Drift Problem</h2>
<p>Most TypeScript APIs end up maintaining three separate descriptions of the same data:</p>
<ol>
<li><p><strong>Runtime validation</strong> checks that run when a request arrives</p>
</li>
<li><p><strong>TypeScript types</strong> <strong>shapes</strong> the compiler understands at build time</p>
</li>
<li><p><strong>API documentation</strong>, the contract you show to consumers</p>
</li>
</ol>
<p>Each one lives in a different file and updates on a different schedule. And none of them can see the others.</p>
<img src="https://cdn.hashnode.com/uploads/covers/63cd01e7cc7a92b9f77dc1e8/1b34f0ca-6b0f-48c0-9282-7bc07d99f171.png" alt="Three API sources of truth often drifting out of sync: TypeScript types, runtime validation, and OpenAPI docs" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p><em>Figure 1: Three descriptions of the same data, maintained separately and often out of sync.</em></p>
<p>Hand-written interfaces disappear at runtime. Joi or Yup schemas validate data but don't give you types for free. OpenAPI files are usually edited by hand, if they're edited at all.</p>
<p>The fix is not "be more careful." The fix is one definition that produces all three outputs.</p>
<img src="https://cdn.hashnode.com/uploads/covers/63cd01e7cc7a92b9f77dc1e8/df2d559a-6afb-469d-81cb-363ab54d0dba.png" alt="One Zod schema producing runtime validation, TypeScript types, and OpenAPI docs" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p><em>Figure 2: One schema definition produces three outputs.</em></p>
<p>That is what Hono and Zod give you when you use them together through <code>@hono/zod-openapi</code>.</p>
<h2 id="heading-2-what-is-hono">2. What Is Hono?</h2>
<p><a href="https://hono.dev">Hono</a> is a small, fast web framework built on Web Standard APIs, the same <code>Request</code> and <code>Response</code> primitives that run in Node.js, Deno, Bun, and Cloudflare Workers.</p>
<p>Compared with Express, that difference matters:</p>
<table>
<thead>
<tr>
<th>Express</th>
<th>Hono</th>
</tr>
</thead>
<tbody><tr>
<td>Node-specific <code>req</code> / <code>res</code></td>
<td>Web Standard APIs</td>
</tr>
<tr>
<td>Params are untyped strings</td>
<td>Params validated and typed with Zod</td>
</tr>
<tr>
<td>Validation is your problem</td>
<td>Route definition is the OpenAPI entry</td>
</tr>
<tr>
<td>Runs on Node only</td>
<td>Runs on Node, Bun, Deno, and the edge</td>
</tr>
</tbody></table>
<p>Hono's core is about 14kb. On Node it's roughly 5–7× faster than Express for the same workload. On Bun or Cloudflare Workers, the gap widens further because those runtimes are optimized for web standards.</p>
<p>For most CRUD APIs, your database is still the bottleneck. But at high concurrency, or on edge runtimes where cold starts matter, the framework gap is real.</p>
<p>More importantly for this tutorial: Hono's OpenAPI integration lets your route definition <em>be</em> the documentation.</p>
<h2 id="heading-3-what-is-zod">3. What Is Zod?</h2>
<p><a href="https://zod.dev">Zod</a> is a TypeScript-first schema validation library. You describe the shape of your data once. Zod then:</p>
<ol>
<li><p>Validates that shape at runtime</p>
</li>
<li><p>Infers the TypeScript type with <code>z.infer</code></p>
</li>
<li><p>Feeds OpenAPI docs when you attach <code>.openapi()</code> metadata</p>
</li>
</ol>
<p>With Joi or Yup, you usually validate at runtime and then hand-write a matching interface. That's two definitions again. Zod removes the second one.</p>
<pre><code class="language-typescript">import { z } from 'zod';

const createTaskSchema = z.object({
  title: z.string().min(1).max(120),
  status: z.enum(['todo', 'in_progress', 'done']).default('todo'),
});

type CreateTaskInput = z.infer&lt;typeof createTaskSchema&gt;;
// { title: string; status?: "todo" | "in_progress" | "done" }
</code></pre>
<p>Change the schema, and every callsite that depends on <code>CreateTaskInput</code> updates with it. TypeScript will tell you what broke.</p>
<h2 id="heading-4-one-schema-three-jobs">4. One Schema, Three Jobs</h2>
<p>Here's the mental model for the rest of the article:</p>
<img src="https://cdn.hashnode.com/uploads/covers/63cd01e7cc7a92b9f77dc1e8/890719d0-6ab0-4529-ad07-565a7db9e604.png" alt="taskSchema feeding runtime validation, TypeScript types, and OpenAPI documentation" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p><em>Figure 3:</em> <code>taskSchema</code> <em>is the single source of truth for validation, types, and docs.</em></p>
<ol>
<li><p><strong>Runtime validation:</strong> bad data is rejected before it reaches your handler, with structured field errors instead of a stack trace</p>
</li>
<li><p><strong>TypeScript types:</strong> <code>z.infer&lt;typeof schema&gt;</code> is derived from the schema, not maintained beside it</p>
</li>
<li><p><strong>OpenAPI docs:</strong> <code>.openapi('Name')</code> registers the schema in the generated spec, so <code>/reference</code> stays current</p>
</li>
</ol>
<p>One definition with three outputs. Nothing to keep in sync by hand.</p>
<h2 id="heading-5-how-to-set-up-the-project">5. How to Set Up the Project</h2>
<p>We'll use the companion demo from <a href="https://github.com/otutukingsley/api-conf-demo">api-conf-demo</a>. It's a small Tasks API that shows the patterns cleanly. Later, we'll look at how the same ideas show up in ClipForge at a larger scale.</p>
<p>Clone the repo and install dependencies:</p>
<pre><code class="language-bash">git clone https://github.com/otutukingsley/api-conf-demo.git
cd api-conf-demo
npm install
</code></pre>
<p>Start the server:</p>
<pre><code class="language-bash">npm run dev
</code></pre>
<p>You should see the API on <code>http://localhost:8080</code>, with interactive docs at <code>/reference</code>.</p>
<p>The important folders look like this:</p>
<pre><code class="language-text">src/
├── db/schema/          # Persistence layer (Drizzle tables)
├── lib/schemas/        # HTTP contract layer (Zod + OpenAPI)
├── lib/errors/         # One error envelope for every failure
├── routes/tasks/       # Route contracts + handlers
├── services/           # Business logic and DB access
├── app.ts              # Middleware, routers, OpenAPI wiring
└── env.ts              # Zod-validated environment config
</code></pre>
<p>This is still MVC. The tooling is just better:</p>
<img src="https://cdn.hashnode.com/uploads/covers/63cd01e7cc7a92b9f77dc1e8/bd701dc8-6f83-490a-905d-fbba09438154.png" alt="MVC-style layers with HTTP client, controller, contract schemas, service model, and database" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p><em>Figure 4: Still MVC. Routes and handlers as the controller, schemas as the contract, and services as the model.</em></p>
<ul>
<li><p><strong>Model</strong> services its own business logic and database access</p>
</li>
<li><p><strong>View / contract</strong> schemas define what data looks like at the DB and HTTP boundaries</p>
</li>
<li><p><strong>Controller</strong> routes declare the contract, handlers fulfill it</p>
</li>
</ul>
<p>A request through the demo API looks like this:</p>
<img src="https://cdn.hashnode.com/uploads/covers/63cd01e7cc7a92b9f77dc1e8/5019c39c-8c45-46f4-8e68-b6388e4c9944.png" alt="Sequence diagram of a POST /tasks request through middleware, route contract, handler, service, and database" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p><em>Figure 5: Request flow through the demo API, including the validation failure path.</em></p>
<p>In that diagram, a request flows through the Client, Middleware, Route contract, Handler, Service, and DB. Middleware handles logging and CORS. The route contract validates the body with Zod.</p>
<p>If validation fails, the client gets a <code>422 ApiError</code> and the handler never runs. If it passes, the handler gets a typed <code>CreateTaskInput</code>, calls <code>TaskService.create()</code>, the service inserts and parses the row, and the client gets <code>201</code> with the Task JSON.</p>
<h2 id="heading-6-how-to-define-your-api-schemas">6. How to Define Your API Schemas</h2>
<p>Start with the HTTP contract in <code>src/lib/schemas/task.ts</code>:</p>
<pre><code class="language-typescript">import { z } from '@hono/zod-openapi';

export const taskStatusSchema = z
  .enum(['todo', 'in_progress', 'done'])
  .openapi('TaskStatus');

export const taskSchema = z
  .object({
    id: z.string().uuid().openapi({
      example: '8e2c9f0a-2222-4a5a-9c3e-1a2b3c4d5e6f',
    }),
    title: z.string().min(1).max(120).openapi({
      example: 'Write the talk abstract',
    }),
    description: z.string().max(2000).nullable().openapi({
      example: 'Cover Hono + Zod patterns',
    }),
    status: taskStatusSchema.default('todo'),
    dueDate: z.string().date().nullable().openapi({
      example: '2026-07-15',
    }),
    createdAt: z.string().openapi({ example: '2026-06-28 10:15:00' }),
    updatedAt: z.string().openapi({ example: '2026-06-28 10:15:00' }),
  })
  .openapi('Task');

export type Task = z.infer&lt;typeof taskSchema&gt;;
</code></pre>
<p>That one object is now the runtime validator, TypeScript type, and an OpenAPI component named <code>Task</code>.</p>
<p>Request schemas should derive from the same base instead of being redeclared:</p>
<pre><code class="language-typescript">export const createTaskSchema = taskSchema
  .pick({ title: true, description: true, status: true, dueDate: true })
  .partial({ description: true, status: true, dueDate: true })
  .openapi('CreateTask');

export const updateTaskSchema = createTaskSchema
  .partial()
  .openapi('UpdateTask');

export type CreateTaskInput = z.infer&lt;typeof createTaskSchema&gt;;
export type UpdateTaskInput = z.infer&lt;typeof updateTaskSchema&gt;;
</code></pre>
<p><code>CreateTask</code> never includes <code>id</code>, because clients don't send one. <code>UpdateTask</code> makes every field optional, because a <code>PATCH</code> can touch any subset.</p>
<img src="https://cdn.hashnode.com/uploads/covers/63cd01e7cc7a92b9f77dc1e8/2f7cd7b3-1f40-4102-83f9-2c682eecc301.png" alt="taskSchema deriving createTaskSchema and updateTaskSchema" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p><em>Figure 6: Request schemas derive from the same base resource schema.</em></p>
<p>We have three contracts with one source.</p>
<h2 id="heading-7-how-to-separate-database-schemas-from-api-schemas">7. How to Separate Database Schemas from API Schemas</h2>
<p>It's tempting to treat the database row and the API response as the same shape. In a tiny demo, they often look identical. In production, they diverge.</p>
<p>Keep them in separate files on purpose:</p>
<img src="https://cdn.hashnode.com/uploads/covers/63cd01e7cc7a92b9f77dc1e8/7a80d075-4f35-467a-8470-3d87448a5982.png" alt="Database persistence schemas mapped to HTTP contract schemas in the service layer" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p><em>Figure 7: Keep database schemas and HTTP schemas as two deliberate layers.</em></p>
<p>Figure 7 has two panels connected by an arrow labeled "map" in the service.</p>
<p>Left pane <code>src/db/schema/</code> (persistence):</p>
<ul>
<li><p>Starts from a Drizzle table definition</p>
</li>
<li><p>That table drives SQL migrations</p>
</li>
<li><p>It also produces drizzle-zod schemas for parsing rows</p>
</li>
<li><p>And row insert/select types for TypeScript at the database boundary</p>
</li>
</ul>
<p>Right panel <code>src/lib/schemas/</code> (HTTP contract):</p>
<ul>
<li><p>Uses Zod + <code>.openapi()</code> as the public contract</p>
</li>
<li><p>Defines request bodies / params clients may send</p>
</li>
<li><p>Defines response bodies clients receive</p>
</li>
<li><p>Registers OpenAPI components used by <code>/doc</code> and <code>/reference</code></p>
</li>
</ul>
<p>The arrow in the middle matters: the database shape isn't automatically the API shape. The <strong>service</strong> maps between them. That's why an internal column can exist on the left without becoming part of the HTTP contract on the right.</p>
<p>In short:</p>
<ul>
<li><p><code>src/db/schema/</code>: what a row looks like in the database</p>
</li>
<li><p><code>src/lib/schemas/</code>: what the HTTP contract looks like</p>
</li>
</ul>
<p>Here's the Drizzle table from the demo:</p>
<pre><code class="language-typescript">import { sql } from 'drizzle-orm';
import { sqliteTable, text } from 'drizzle-orm/sqlite-core';
import { createInsertSchema, createSelectSchema } from 'drizzle-zod';
import { z } from 'zod';

export const taskStatusValues = ['todo', 'in_progress', 'done'] as const;

export const tasks = sqliteTable('tasks', {
  id: text('id').primaryKey(),
  title: text('title').notNull(),
  description: text('description'),
  status: text('status', { enum: taskStatusValues }).notNull().default('todo'),
  dueDate: text('due_date'),
  createdAt: text('created_at')
    .notNull()
    .default(sql`(current_timestamp)`),
  updatedAt: text('updated_at')
    .notNull()
    .default(sql`(current_timestamp)`),
});

export const selectTaskSchema = createSelectSchema(tasks, {
  status: z.enum(taskStatusValues),
});

export const insertTaskSchema = createInsertSchema(tasks, {
  id: () =&gt; z.string().uuid().optional(),
  title: () =&gt; z.string().min(1).max(120),
  description: () =&gt; z.string().max(2000).nullable().optional(),
  status: z.enum(taskStatusValues).optional(),
  dueDate: () =&gt; z.string().date().nullable().optional(),
});
</code></pre>
<p>One table definition gives you three outputs:</p>
<ol>
<li><p><strong>TypeScript types</strong> inferred from the columns</p>
</li>
<li><p><strong>SQL migrations</strong> generated with <code>drizzle-kit</code></p>
</li>
<li><p><strong>Zod schemas</strong> via <code>drizzle-zod</code></p>
</li>
</ol>
<p>That's useful at the database boundary. It's not a replacement for your HTTP schemas.</p>
<p>The moment you add an internal <code>archivedAt</code> column, or a computed field the API returns that's not a column, the split pays for itself. Divergence becomes a normal change instead of a painful refactor.</p>
<img src="https://cdn.hashnode.com/uploads/covers/63cd01e7cc7a92b9f77dc1e8/b055d171-05a7-4661-8a52-73db3f28c8b5.png" alt="Service mapping a database row with internal fields to a public API response" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p><em>Figure 8: The service decides what the client may see.</em></p>
<h2 id="heading-8-how-to-define-routes-as-contracts">8. How to Define Routes as Contracts</h2>
<p>In this architecture, a route doesn't "just handle a request." A route declares the contract: method, path, request schemas, and response schemas.</p>
<img src="https://cdn.hashnode.com/uploads/covers/63cd01e7cc7a92b9f77dc1e8/b8a19630-7447-4a9e-a643-3c950d7a88cc.png" alt="Route definition acting as the API contract fulfilled by a handler" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p><em>Figure 9: The route declares the contract. The handler fulfills it.</em></p>
<pre><code class="language-typescript">import { createRoute, z } from '@hono/zod-openapi';
import * as HttpStatusCodes from '@/lib/http-status-codes';
import { jsonContent } from '@/lib/openapi/json-content';
import { jsonApiErrorContent } from '@/lib/openapi/error-schema';
import {
  createTaskSchema,
  taskParamsSchema,
  taskSchema,
} from '@/lib/schemas/task';

export const createTask = createRoute({
  tags: ['Tasks'],
  method: 'post',
  path: '/tasks',
  summary: 'Create a task',
  request: {
    body: jsonContent(createTaskSchema, 'The task to create'),
  },
  responses: {
    [HttpStatusCodes.CREATED]: jsonContent(taskSchema, 'The created task'),
    [HttpStatusCodes.UNPROCESSABLE_ENTITY]:
      jsonApiErrorContent('Validation error'),
    [HttpStatusCodes.INTERNAL_SERVER_ERROR]: jsonApiErrorContent(
      'Internal server error',
    ),
  },
});

export const getTask = createRoute({
  tags: ['Tasks'],
  method: 'get',
  path: '/tasks/{id}',
  summary: 'Get a task by ID',
  request: {
    params: taskParamsSchema,
  },
  responses: {
    [HttpStatusCodes.OK]: jsonContent(taskSchema, 'The requested task'),
    [HttpStatusCodes.NOT_FOUND]: jsonApiErrorContent('Task not found'),
    [HttpStatusCodes.UNPROCESSABLE_ENTITY]:
      jsonApiErrorContent('Validation error'),
    [HttpStatusCodes.INTERNAL_SERVER_ERROR]: jsonApiErrorContent(
      'Internal server error',
    ),
  },
});
</code></pre>
<p>A small helper keeps response boilerplate readable:</p>
<pre><code class="language-typescript">export function jsonContent&lt;T extends z.ZodTypeAny&gt;(
  schema: T,
  description: string,
) {
  return {
    content: {
      'application/json': { schema },
    },
    description,
  };
}
</code></pre>
<p>Named status constants replace magic numbers. You use the same constants as object keys in the route and as switch cases in the handler. That makes status codes searchable and consistent.</p>
<h2 id="heading-9-how-to-keep-handlers-thin">9. How to Keep Handlers Thin</h2>
<p>Once the route defines the contract, the handler only has to fulfill it.</p>
<pre><code class="language-typescript">export const getTask: AppRouteHandler&lt;GetTaskRoute&gt; = (c) =&gt; {
  try {
    const { id } = c.req.valid('param');
    return c.json(TaskService.get(id), HttpStatusCodes.OK);
  } catch (error) {
    const apiError = ApiError.parse(error);
    switch (apiError.statusCode) {
      case HttpStatusCodes.NOT_FOUND:
      case HttpStatusCodes.UNPROCESSABLE_ENTITY:
        return c.json(apiError.toResponseBody(), apiError.statusCode);
      default:
        return c.json(
          apiError.toResponseBody(),
          HttpStatusCodes.INTERNAL_SERVER_ERROR,
        );
    }
  }
};

export const createTask: AppRouteHandler&lt;CreateTaskRoute&gt; = (c) =&gt; {
  try {
    const body = c.req.valid('json');
    return c.json(TaskService.create(body), HttpStatusCodes.CREATED);
  } catch (error) {
    const apiError = ApiError.parse(error);
    switch (apiError.statusCode) {
      case HttpStatusCodes.UNPROCESSABLE_ENTITY:
        return c.json(apiError.toResponseBody(), apiError.statusCode);
      default:
        return c.json(
          apiError.toResponseBody(),
          HttpStatusCodes.INTERNAL_SERVER_ERROR,
        );
    }
  }
};
</code></pre>
<p>Notice what's <em>not</em> in the handler:</p>
<ul>
<li><p>No manual parsing of params or bodies</p>
</li>
<li><p>No casting to <code>any</code></p>
</li>
<li><p>No business logic</p>
</li>
</ul>
<p><code>c.req.valid('param')</code> and <code>c.req.valid('json')</code> are already validated and typed. The service owns the database work:</p>
<pre><code class="language-typescript">get(id: string): Task {
  try {
    const row = db.select().from(tasks).where(eq(tasks.id, id)).get();
    if (!row) throw new NotFoundError(`Task ${id} not found`);
    return selectTaskSchema.parse(row);
  } catch (error) {
    throw ApiError.parse(error);
  }
},
</code></pre>
<p>The service wraps its body in one <code>try/catch</code> and normalizes every failure through <code>ApiError.parse()</code>. Zod errors, custom domain errors, and unexpected driver errors all become one typed shape.</p>
<h2 id="heading-10-how-to-return-one-error-shape-everywhere">10. How to Return One Error Shape Everywhere</h2>
<p>Clients should never guess whether an error looks like <code>{ message }</code>, <code>{ error }</code>, or a raw stack trace.</p>
<p>In the demo, every failure becomes one envelope:</p>
<img src="https://cdn.hashnode.com/uploads/covers/63cd01e7cc7a92b9f77dc1e8/85080f92-0fde-45cf-9b2e-eeb4f7ec5ef5.png" alt="Different error sources normalized through ApiError.parse into one JSON envelope" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p><em>Figure 10: Every failure path becomes one predictable error shape.</em></p>
<p>The factory that creates routers bakes that behavior in with <code>defaultHook</code>:</p>
<pre><code class="language-typescript">export function createRouter() {
  return new OpenAPIHono({
    defaultHook: (result, c) =&gt; {
      if (!result.success) {
        const apiError = ApiError.parse(result.error);
        return c.json(apiError.toResponseBody(), apiError.statusCode);
      }
    },
  });
}
</code></pre>
<p>Every router goes through that factory. A bad UUID in a path param, a missing field in a POST body, or an invalid enum in a query string all produce the same response shape before the handler runs.</p>
<p><code>ApiError.parse()</code> is the second half of the pattern:</p>
<pre><code class="language-typescript">public static parse(error: unknown): ApiError {
  if (error instanceof ApiError) return error;

  if (error instanceof ZodError) {
    return new ApiError('Validation error', {
      statusCode: 422,
      errors: error.flatten().fieldErrors,
    });
  }

  return new ApiError('Internal server error', { statusCode: 500 });
}
</code></pre>
<p>Handlers still use an explicit <code>switch</code> on status codes. That's intentional. Each route documents exactly which statuses it can return. A handler that should never emit <code>403</code> doesn't have <code>403</code> sitting in a shared helper's defaults.</p>
<p>The repetition is the point. Explicit beats clever here.</p>
<h2 id="heading-11-how-to-generate-docs-that-cant-drift">11. How to Generate Docs That Can't Drift</h2>
<p>Because schemas are attached to route definitions, OpenAPI becomes a byproduct of the code instead of a separate chore.</p>
<img src="https://cdn.hashnode.com/uploads/covers/63cd01e7cc7a92b9f77dc1e8/aac24756-84db-48f2-936a-3370c36b3dd2.png" alt="Zod schemas flowing into route definitions, OpenAPI JSON, and Scalar reference UI" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p><em>Figure 11: Docs are generated from the same route definitions as the runtime code.</em></p>
<pre><code class="language-typescript">export function configureOpenAPI(app: OpenAPIHono) {
  app.doc('/doc', {
    openapi: '3.0.0',
    info: {
      title: 'Bulletproof Tasks API',
      version: '1.0.0',
    },
  });

  app.get(
    '/reference',
    apiReference({
      spec: { url: '/doc' },
      theme: 'kepler',
      layout: 'modern',
      pageTitle: 'Bulletproof Tasks API',
    }),
  );
}
</code></pre>
<ul>
<li><p><code>GET /doc</code> returns the raw OpenAPI JSON</p>
</li>
<li><p><code>GET /reference</code> serves an interactive Scalar explorer</p>
</li>
</ul>
<p>When you change a schema or a response code in the route file, the docs update with it. There's no second docs step to forget.</p>
<h2 id="heading-12-how-to-make-the-app-production-ready">12. How to Make the App Production-Ready</h2>
<p>Type safety at the request boundary isn't enough. Configuration and process lifecycle need the same discipline.</p>
<h3 id="heading-validate-environment-variables-at-boot">Validate Environment Variables at Boot</h3>
<pre><code class="language-typescript">import { z } from 'zod';

const envSchema = z.object({
  NODE_ENV: z
    .enum(['development', 'test', 'production'])
    .default('development'),
  LOG_LEVEL: z
    .enum(['silent', 'debug', 'info', 'warn', 'error', 'fatal'])
    .default('info'),
  PORT: z.coerce.number().default(8080),
  DATABASE_URL: z.string().default('tasks.db'),
});

export const env = envSchema.parse(process.env);
</code></pre>
<p>If a required value is missing or malformed, the process exits immediately with a clear Zod error. That's much better than discovering <code>undefined</code> three requests into production.</p>
<h3 id="heading-prefer-structured-logging">Prefer Structured Logging</h3>
<p>The demo uses Pino through <code>hono-pino</code> instead of a one-line console logger. In production you want JSON logs. In development you want something readable. One middleware can do both, and every request can carry a UUID.</p>
<h3 id="heading-shut-down-cleanly">Shut Down Cleanly</h3>
<p>Docker and process managers send <code>SIGTERM</code> before they kill a process. Handle it. Close the HTTP server, then close the database handle, then exit. Without that, a SQLite file (or a Postgres connection pool) can be left in a dirty state.</p>
<h3 id="heading-keep-the-app-runtime-portable">Keep the App Runtime-Portable</h3>
<p>Hono only depends on Web Standard APIs inside <code>app</code>. That means the same application object can run on Node:</p>
<pre><code class="language-typescript">import { serve } from '@hono/node-server';
import { app } from '@/app';

serve({ fetch: app.fetch, port: env.PORT });
</code></pre>
<p>Or on an edge runtime with almost nothing else:</p>
<pre><code class="language-typescript">import { app } from '@/app';

export default app;
</code></pre>
<p>That;s not a simplified example. That's the whole adapter.</p>
<h2 id="heading-13-how-these-patterns-scale-in-a-production-app">13. How These Patterns Scale in a Production App</h2>
<p>A Tasks API is a good teaching surface. Production systems are messier: you have uploads, background jobs, multiple packages, and longer-lived workflows.</p>
<p><a href="https://github.com/otutukingsley/clipforge">ClipForge</a> is a helpful example of a production app where those same ideas show up at scale. It's a self-hostable video processing toolkit built with Node.js, Hono, Zod, Drizzle, BullMQ, and Nuxt. You upload a video and get transcription, subtitles, summaries, chapter markers, and thumbnails.</p>
<p>The architecture looks like this:</p>
<pre><code class="language-text">apps/
├── api/        # Hono REST API
├── worker/     # BullMQ video processor
└── web/        # Nuxt frontend
packages/
├── shared/     # Zod schemas, constants, shared types
└── providers/  # OpenAI, Anthropic, Deepgram integrations
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/63cd01e7cc7a92b9f77dc1e8/f341110f-ef93-4b79-b0d0-9b07aeed04d2.png" alt="ClipForge architecture with web, API, worker, shared schemas, Postgres, Redis, and AI providers" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p><em>Figure 12: ClipForge keeps the same contract-first core across web, API, and worker packages.</em></p>
<p>A video job moves through stages like uploading, validating, extracting audio, and so on without inventing new response shapes along the way:</p>
<img src="https://cdn.hashnode.com/uploads/covers/63cd01e7cc7a92b9f77dc1e8/38487b12-26fd-4185-9ca1-48560a03699e.png" alt="ClipForge video job stages from uploading through complete, with a failed path" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p><em>Figure 13: A video job moves through typed stages without inventing new response shapes.</em></p>
<p>The important part isn't the video pipeline. The important part is that the API still follows the same contract-first rules.</p>
<h3 id="heading-shared-zod-schemas-across-packages">Shared Zod Schemas Across Packages</h3>
<p>ClipForge keeps HTTP contracts in <code>packages/shared</code>, so the API, worker, and web app share one vocabulary:</p>
<pre><code class="language-typescript">export const processingFeatureSchema = z.enum([
  'transcription',
  'subtitles',
  'summary',
  'chapters',
  'thumbnails',
]);

export const videoUploadSchema = z.object({
  title: z.string().min(3).max(200),
  description: z.string().max(2000).optional(),
  features: z
    .array(processingFeatureSchema)
    .default([
      'transcription',
      'subtitles',
      'summary',
      'chapters',
      'thumbnails',
    ]),
  priority: z.enum(['low', 'normal', 'high']).default('normal'),
});

export const jobStatusSchema = z.object({
  jobId: z.string(),
  title: z.string().optional(),
  state: jobStateSchema,
  stage: jobStageSchema,
  progress: z.number().min(0).max(100),
  result: jobResultSchema.optional(),
  failedReason: z.string().optional(),
  createdAt: z.string(),
  updatedAt: z.string(),
});
</code></pre>
<p>When the worker finishes a stage, it doesn't invent a new response shape. It updates state against the same schemas the API returns to the client.</p>
<h3 id="heading-routes-still-define-the-contract">Routes Still Define the Contract</h3>
<p>The upload route in ClipForge looks like the Tasks demo, just with multipart form data and rate limiting:</p>
<pre><code class="language-typescript">export const uploadVideo = createRoute({
  tags: ['videos'],
  method: 'post',
  path: '/videos/upload',
  middleware: [
    sessionMiddleware,
    apiKeysMiddleware,
    rateLimitMiddleware({
      windowMs: 60_000,
      max: 10,
      keyPrefix: 'ratelimit:upload',
    }),
  ] as const,
  request: {
    body: {
      content: {
        'multipart/form-data': {
          schema: z.object({
            file: z.instanceof(File),
            title: z.string().min(3).max(200),
            description: z.string().max(2000).optional(),
            features: z.string().optional(),
            priority: z.enum(['low', 'normal', 'high']).optional(),
          }),
        },
      },
    },
  },
  responses: {
    [HTTP_STATUS.ACCEPTED]: jsonContent(
      jobStatusSchema,
      'Video accepted for processing',
    ),
    [HTTP_STATUS.BAD_REQUEST]: jsonApiErrorContent('Invalid request'),
    [HTTP_STATUS.UNPROCESSABLE]: jsonApiErrorContent('Validation error'),
    [HTTP_STATUS.TOO_MANY_REQUESTS]: jsonApiErrorContent('Rate limit exceeded'),
  },
});
</code></pre>
<p>The handler validates input, writes a job row, enqueues BullMQ work, and returns <code>202 Accepted</code> with a typed job status. The heavy lifting happens in the worker. The API stays a contract layer.</p>
<img src="https://cdn.hashnode.com/uploads/covers/63cd01e7cc7a92b9f77dc1e8/5d3d8157-b1c0-4f40-82f1-9023de847a24.png" alt="Sequence diagram of ClipForge video upload, queue processing, and job status polling" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p><em>Figure 14: The API accepts the upload and returns a typed job status while the worker does the heavy lifting.</em></p>
<p>Figure 14 is a sequence diagram with five participants: Web UI, Hono API, BullMQ, Worker, and Postgres. It shows the async boundary between “accept the upload” and “finish processing.” Here's every step in the diagram:</p>
<ol>
<li><p>The Web UI sends <code>POST /videos/upload</code> to the Hono API.</p>
</li>
<li><p>The Hono API runs <code>Zod + middleware</code> (validate input, session, rate limits, and related checks).</p>
</li>
<li><p>The Hono API performs <code>insert job row</code> in Postgres so the job exists before work starts.</p>
</li>
<li><p>The Hono API sends <code>enqueue process-video</code> to BullMQ.</p>
</li>
<li><p>The Hono API immediately returns <code>202 + jobStatusSchema</code> to the Web UI. At this point the client has a typed job status, but transcription hasn't finished yet.</p>
</li>
<li><p>BullMQ later delivers <code>process job</code> to the Worker.</p>
</li>
<li><p>The Worker runs <code>transcribe / analyze / thumbnails</code> as background work.</p>
</li>
<li><p>The Worker writes <code>update stage + result</code> back to Postgres as stages complete.</p>
</li>
<li><p>The Web UI polls with <code>GET /videos/jobs/{id}</code> against the Hono API.</p>
</li>
<li><p>The Hono API reads the latest job from Postgres and returns <code>jobStatusSchema</code> agai the same response shape as step 5, now with updated <code>stage</code>, <code>progress</code>, and eventually <code>result</code>.</p>
</li>
</ol>
<p>So the API stays a fast contract layer: accept, persist, enqueue, and respond. The worker owns the slow pipeline. The UI tracks progress by polling one share status schema.</p>
<h3 id="heading-database-schemas-still-stay-separate">Database Schemas Still Stay Separate</h3>
<p>ClipForge stores jobs in Postgres with Drizzle:</p>
<pre><code class="language-typescript">export const jobs = pgTable('jobs', {
  id: varchar('id', { length: 36 }).primaryKey(),
  sessionId: varchar('session_id', { length: 36 }).notNull(),
  state: jobStateEnum('state').notNull().default('waiting'),
  stage: jobStageEnum('stage').notNull().default('uploading'),
  progress: integer('progress').notNull().default(0),
  title: varchar('title', { length: 200 }).notNull(),
  features: jsonb('features').$type&lt;string[]&gt;().notNull(),
  provider: varchar('provider', { length: 50 }).notNull().default('openai'),
  result: jsonb('result').$type&lt;JobResult&gt;(),
  failedReason: text('failed_reason'),
  createdAt: timestamp('created_at', { withTimezone: true })
    .notNull()
    .defaultNow(),
  updatedAt: timestamp('updated_at', { withTimezone: true })
    .notNull()
    .defaultNow(),
});
</code></pre>
<p>The table has persistence concerns like <code>filePath</code> and <code>sessionId</code>. The public <code>jobStatusSchema</code> doesn't have to expose all of them. That's the same DB-versus-API split from the Tasks demo, applied to a real workflow.</p>
<h3 id="heading-the-same-production-habits-still-apply">The Same Production Habits Still Apply</h3>
<p>ClipForge validates environment variables with Zod on boot, configures OpenAPI and Scalar at <code>/doc</code> and <code>/reference</code>, normalizes failures through <code>ApiError</code>, and shuts down queues and Redis on <code>SIGTERM</code>.</p>
<p>The lesson is simple: if the small app is structured correctly, the large app doesn't need a different philosophy. It needs more packages, more middleware, and longer-running jobs on top of the same contract-first core.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Type-safe APIs aren't about adding more TypeScript. They're about removing duplicate sources of truth.</p>
<p>With Hono and Zod you can:</p>
<ul>
<li><p>validate requests at runtime</p>
</li>
<li><p>infer types automatically</p>
</li>
<li><p>generate OpenAPI docs from the same route definitions</p>
</li>
<li><p>keep database schemas and HTTP schemas intentionally separate</p>
</li>
<li><p>return one predictable error shape from every failure path</p>
</li>
</ul>
<p>Start with the <a href="https://github.com/otutukingsley/api-conf-demo">api-conf-demo</a> if you want the smallest readable version of these patterns. Then look at <a href="https://github.com/otutukingsley/clipforge">ClipForge</a> to see how the same ideas hold up when the API sits in front of uploads, queues, and multi-stage processing.</p>
<p>Once your route definition is the contract, your docs stop drifting, your handlers get thinner, and your clients get an API they can trust.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Use Zod for React API Validation ]]>
                </title>
                <description>
                    <![CDATA[ In React applications, handling API (Application Programming Interface) responses can be challenging. You might encounter data that’s missing crucial fields, that’s formatted unexpectedly, or that simply doesn’t match what you anticipated. This incon... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-use-zod-for-react-api-validation/</link>
                <guid isPermaLink="false">67c1f994107ebba152ad9e79</guid>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                    <category>
                        <![CDATA[ zod ]]>
                    </category>
                
                    <category>
                        <![CDATA[ API ]]>
                    </category>
                
                    <category>
                        <![CDATA[ TypeScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ frontend ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Emore Ogheneyoma Lawrence ]]>
                </dc:creator>
                <pubDate>Fri, 28 Feb 2025 17:59:48 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1740756200896/a57c4e95-b13e-412a-828e-09e97f22a6c4.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>In React applications, handling API (Application Programming Interface) responses can be challenging. You might encounter data that’s missing crucial fields, that’s formatted unexpectedly, or that simply doesn’t match what you anticipated.</p>
<p>This inconsistency can lead to errors in your code and make it difficult to work with the data effectively. Imagine wrestling with unpredictable API responses as your application grows – it can quickly become a development nightmare!</p>
<p>This is where Zod comes in, offering a solution to effectively manage API data validation within React.</p>
<h3 id="heading-by-the-end-of-this-tutorial-youll-learn-how-to">By the end of this tutorial, you’ll learn how to:</h3>
<ol>
<li><p>Set up and use Zod for API response validation in React.</p>
</li>
<li><p>Define schemas to validate and transform incoming data.</p>
</li>
<li><p>integrate Zod into API calls to improve data handling and prevent UI crashes.</p>
</li>
</ol>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-what-is-zod-and-why-use-it-for-react-api-calls">What is Zod, and Why Use it for React API Calls?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-generate-a-new-typescript-react-project">How to Generate a New TypeScript React Project</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-core-zod-concepts-basic-usage-types-and-validation">Core Zod Concepts: Basic Usage, Types, and Validation</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-build-zod-schemas-for-api-responses">How to Build Zod Schemas for API Responses</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-integrate-zod-with-react-api-calls">How to Integrate Zod with React API Calls</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-render-the-user-interface-ui-and-handle-errors-in-react">How to Render the User Interface (UI) and Handle Errors in React</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-what-is-zod-and-why-use-it-for-react-api-calls">What is Zod, and Why Use it for React API Calls?</h2>
<p>Zod is a powerful TypeScript-first library that simplifies data validation. It lets you define clear rules (schemas) for your expected data format.</p>
<p>Zod can then validate incoming data (often from API responses) to ensure it conforms to these rules. This validation process guarantees that the data adheres to your defined format, enhancing the reliability and integrity of your application.</p>
<p>Here's why Zod shines for React API validation:</p>
<ul>
<li><p>Clear schemas: Zod helps you define concise blueprints for API responses, enhancing readability and maintainability.</p>
</li>
<li><p>Data validation: It offers powerful validation methods for various data types, enforcing rules like required fields and specific formats.</p>
</li>
<li><p>Early error detection: It helps you detect data inconsistencies during API calls, preventing unexpected errors later in the application.</p>
</li>
<li><p>Improved developer experience: It promotes type-safe coding, streamlining development time by eliminating manual data type checks.</p>
</li>
<li><p>Single source of truth: And finally, Zod serves as a central point for data model definitions, ensuring consistency across the React application and reducing errors.</p>
</li>
</ul>
<p>Using Zod, you can transform unpredictable API responses into clean, structured data, setting the stage for a smoother and more efficient development experience in your React applications.</p>
<h2 id="heading-how-to-generate-a-new-typescript-react-project">How to Generate a New TypeScript React Project</h2>
<p>Creating a new React project with TypeScript is straightforward. Here's how to get started. Execute the following command in your terminal:</p>
<pre><code class="lang-bash">npm create vite@latest my-react-app -- --template react-ts
</code></pre>
<p>Once the project is generated, navigate to the projects directory:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> my-react-app
npm install
npm run dev
</code></pre>
<p>That’s it! Your React Project with TypeScript is now up and ready to use. Run the command below to install the Zod package:</p>
<pre><code class="lang-bash">npm install zod
</code></pre>
<h2 id="heading-core-zod-concepts-basic-usage-types-and-validation">Core Zod Concepts: Basic Usage, Types, and Validation</h2>
<p>Zod helps you define clear expectations for your API responses using <strong>schemas</strong>. These schemas act like blueprints, specifying the types of data you expect to receive.</p>
<h3 id="heading-how-to-build-schemas">How to Build Schemas</h3>
<p>Zod provides builder functions like <code>z.string()</code>, <code>z.number()</code>, and <code>z.object()</code> to create schemas. These functions define the data type you want for a specific field in your response.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { z } <span class="hljs-keyword">from</span> <span class="hljs-string">'zod'</span>;
<span class="hljs-comment">// Define basic data types</span>
<span class="hljs-keyword">const</span> userName = z.string().min(<span class="hljs-number">5</span>).max(<span class="hljs-number">10</span>); <span class="hljs-comment">// String with min 5 and max 10 characters</span>
<span class="hljs-keyword">const</span> userAge = z.number().positive().int();  <span class="hljs-comment">// Positive integer</span>
<span class="hljs-keyword">const</span> userEmail = z.string().email();        <span class="hljs-comment">// Ensures a valid email format</span>

<span class="hljs-built_in">console</span>.log(userName.parse(<span class="hljs-string">'John Doe'</span>));       <span class="hljs-comment">// Output: John Doe (valid)</span>
<span class="hljs-built_in">console</span>.log(userAge.parse(<span class="hljs-number">30</span>));              <span class="hljs-comment">// Output: 30 (valid)</span>
<span class="hljs-built_in">console</span>.log(userEmail.parse(<span class="hljs-string">"johnDoe@gmail.com"</span>)); <span class="hljs-comment">// Output: johnDoe@gmail.com (valid)</span>
</code></pre>
<p>The code above defines three basic data types:</p>
<ul>
<li><p><code>userName</code>: Represents a string with a minimum length of 5 characters and a maximum length of 10 characters.</p>
</li>
<li><p><code>userAge</code>: Represents a positive integer.</p>
</li>
<li><p><code>userEmail</code>: Ensures a valid email format.</p>
</li>
</ul>
<p>Here’s the result of the code above:  </p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738857987322/55a5943c-6633-4432-a441-c3dfa9400d93.png" alt="Image of the resulting code above" class="image--center mx-auto" width="337" height="85" loading="lazy"></p>
<h3 id="heading-how-to-add-validation-rules">How to Add Validation Rules</h3>
<p>Zod allows you to chain methods like <strong>min</strong>, <strong>max</strong>, <strong>positive</strong>, <strong>int</strong>, and <strong>email</strong> to enforce specific rules on these data types. Here’s an example of an invalid string exceeding the maximum length:</p>
<pre><code class="lang-typescript"><span class="hljs-built_in">console</span>.log(userName.parse(<span class="hljs-string">"Hello there, My Name is John Doe"</span>)); <span class="hljs-comment">// Throws ZodError</span>
</code></pre>
<p>The code throws a <code>ZodError</code> due to exceeding the maximum length of 10 strings, disrupting our application flow and eventually causing our application to break.  </p>
<p>Here’s the image of the resulting code error:  </p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738859384410/27c2a5f3-2410-4709-ac27-8bf24f6c3fd8.png" alt="Image of the resulting code above" class="image--center mx-auto" width="325" height="225" loading="lazy"></p>
<h3 id="heading-validating-and-parsing">Validating and Parsing</h3>
<p>Zod offers two ways to check data against your schema:</p>
<ul>
<li><p><code>schema.parse(data)</code><strong>:</strong> This method attempts to parse the data according to your schema. But if there's a validation error, it throws a <code>ZodError</code>. This can disrupt your application's flow, as illustrated in the previous example.</p>
</li>
<li><p><code>schema.safeParse(data)</code>: This is the recommended approach. it parses the data and returns a <code>ZodResult object</code>. This object contains some key properties:</p>
<ul>
<li><p><code>success</code>: A boolean indicating whether the parsing was successful.</p>
</li>
<li><p><code>data</code>: The parsed data itself (if the success property is true)</p>
</li>
<li><p><code>error</code>: An error message if validation fails (if success property is false)</p>
</li>
</ul>
</li>
</ul>
<p>Here are two examples showcasing the usage of <code>safeParse</code> with both valid and invalid data so you can see the resulting outcomes.  </p>
<p>First, lets see an example using <code>safeParse</code> with valid data:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> userSchema = z.object({
  name: userName,
  age: userAge,
  email: userEmail,
});

<span class="hljs-keyword">const</span> userData = {
  name: <span class="hljs-string">"John Doe"</span>,
  age: <span class="hljs-number">24</span>,
  email: <span class="hljs-string">"johndoe@gmail.com"</span>
};

<span class="hljs-keyword">const</span> result = userSchema.safeParse(userData);

<span class="hljs-built_in">console</span>.log(result); <span class="hljs-comment">// ZodObject containing data and success status</span>
</code></pre>
<p>This code defines a schema for user data using Zod, including properties for name, age, and email. It then attempts to parse a sample <code>userData</code> object using this schema via <code>safeParse()</code>. If successful, it prints the parsed data – otherwise, it logs an error message indicating the use of invalid data.  </p>
<p>Here’s the image of the resulting code above:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738861119776/7f9cf3ec-fc83-452c-9477-1c7d6422efe3.png" alt="7f9cf3ec-fc83-452c-9477-1c7d6422efe3" class="image--center mx-auto" width="356" height="140" loading="lazy"></p>
<p>Let’s now see how <code>safeParse()</code> handles invalid data using the same example as above. We’ll pass invalid data to the <code>userSchema.safeParse()</code> function to observe its behaviour.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> userSchema = z.object({
  name: userName,
  age: userAge,
  email: userEmail,
});

<span class="hljs-keyword">const</span> userData = {
  name: <span class="hljs-string">"John Doe"</span>,
  age: <span class="hljs-number">24</span>,
  email: <span class="hljs-string">"johndoe.com"</span> <span class="hljs-comment">// invalid email</span>
};

<span class="hljs-keyword">const</span> result = userSchema.safeParse(userData);

<span class="hljs-built_in">console</span>.log(result); <span class="hljs-comment">// ZodObject containing error and success status</span>
</code></pre>
<p>In this code example, we defined the <code>userSchema</code>. Next, we attempted to parse the <code>userData</code> object. But the parsing failed because the email property was not correctly formatted. Here’s a visual representation of the resulting output:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1740502631913/25dead7a-ab09-4d1d-9dcc-fada38e7d4d6.png" alt="25dead7a-ab09-4d1d-9dcc-fada38e7d4d6" class="image--center mx-auto" width="367" height="217" loading="lazy"></p>
<p>Unlike using <code>parse</code>, which completely halts your application upon encountering validation errors and throws a <code>ZodError</code>, utilizing <code>safeParse</code> allows you to gracefully handle these errors, ensuring uninterrupted operation.</p>
<h2 id="heading-how-to-build-zod-schemas-for-api-responses">How to Build Zod Schemas for API Responses</h2>
<p>Building on our understanding of Zod’s core concepts, let’s create Zod schemas specifically for data received from API calls. We’ll leverage data from <a target="_blank" href="https://jsonplaceholder.typicode.com/posts">JSONPlaceholder</a>, which offers information about posts.  </p>
<p>Here’s a sample JSON response representing a post from JSONPlaceholder:</p>
<pre><code class="lang-typescript">{
  <span class="hljs-string">"userId"</span>: <span class="hljs-number">1</span>,
  <span class="hljs-string">"id"</span>: <span class="hljs-number">3</span>,
  <span class="hljs-string">"title"</span>: <span class="hljs-string">"ea molestias quasi exercitationem repellat qui ipsa sit aut"</span>,
  <span class="hljs-string">"body"</span>: <span class="hljs-string">"et iusto sed quo iure\nvoluptatem occaecati omnis eligendi aut"</span>
}
</code></pre>
<p>Create a React component (give it a name that fits your project structure) to demonstrate building and utilizing Zod schemas for API validation. In this article, for illustrative purposes, we’ll call it the <code>ZodApi</code> component.</p>
<pre><code class="lang-typescript"> <span class="hljs-keyword">import</span> { z } <span class="hljs-keyword">from</span> <span class="hljs-string">'zod'</span>;

  <span class="hljs-keyword">const</span> postSchema = z.object({
  userId: z.number().positive().int(),
  id: z.number().positive().int(),
  title: z.string(),
  body: z.string()
});

<span class="hljs-keyword">const</span> postSchemaArray = z.array(postSchema); <span class="hljs-comment">// Schema for array of posts</span>
</code></pre>
<p>This code defines the expected structure of a single post object (<code>postSchema</code>) and an array of posts (<code>postSchemaArray</code>).</p>
<p>The following sections will explore integrating Zod with React components for API call handling and error management.</p>
<h2 id="heading-how-to-integrate-zod-with-react-api-calls">How to Integrate Zod with React API Calls</h2>
<p>Let's bridge the gap between your defined Zod schemas and real API interactions.</p>
<p>We’ll need to update the code we wrote in the previous section to achieve our desired result in this section.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { z } <span class="hljs-keyword">from</span> <span class="hljs-string">'zod'</span>;
<span class="hljs-keyword">import</span> { useEffect } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;

<span class="hljs-keyword">const</span> postSchema = z.object({
  userId: z.number().positive().int(),
  id: z.number().positive().int(),
  title: z.string(),
  body: z.string()
});

<span class="hljs-keyword">const</span> postSchemaArray = z.array(postSchema); <span class="hljs-comment">// schema for an array of posts</span>

<span class="hljs-keyword">type</span> Posts = z.infer&lt;<span class="hljs-keyword">typeof</span> postSchemaArray&gt;; <span class="hljs-comment">// type of the posts</span>

<span class="hljs-keyword">const</span> ZodApi = <span class="hljs-function">() =&gt;</span> {
  useEffect(<span class="hljs-function">() =&gt;</span> {
    fetch(<span class="hljs-string">"https://jsonplaceholder.typicode.com/posts"</span>)
      .then(<span class="hljs-function">(<span class="hljs-params">response</span>) =&gt;</span> response.json())
      .then(<span class="hljs-function">(<span class="hljs-params">posts: Posts</span>) =&gt;</span> {
        <span class="hljs-keyword">const</span> validatedPosts = postSchemaArray.safeParse(posts); <span class="hljs-comment">// remember to use safeParse instead of parse</span>

        <span class="hljs-keyword">if</span> (validatedPosts.success === <span class="hljs-literal">false</span>) {
          <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Validation Error:"</span>validatedPosts.error);
          <span class="hljs-keyword">return</span>;
        }

        <span class="hljs-comment">// we can now safely use the posts</span>
        <span class="hljs-built_in">console</span>.log(validatedPosts.data);
      });
  }, []);
  <span class="hljs-keyword">return</span> &lt;div&gt;ZodApi&lt;/div&gt;;
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> ZodApi;
</code></pre>
<p>The <code>ZodApi</code> component demonstrates:</p>
<ul>
<li><p>Fetching data: Uses <code>useEffect</code> and <code>fetch</code> to get data from the API.</p>
</li>
<li><p>Type safety: <code>type Posts = z.infer&lt;typeof postSchemaArray&gt;;</code> ensures type safety by defining the <code>Posts type</code> inferred from the schema <code>postSchemaArray</code>.</p>
</li>
<li><p>Parsing with Zod: Validates the fetched data against the <code>postSchemaArray</code> using <code>safeParse</code>.</p>
</li>
<li><p>Handling success: If validation succeeds, it provides access to clean data in <code>validatedPosts.data</code> for use in your component (UI, state, and so on).</p>
</li>
</ul>
<p>Error handling: The <code>if</code> statement showcases a simple approach to Zod error handling. In a case where the validation is not successful (<code>validatedPosts.success === false</code>), a ZodError message is logged to the console.  </p>
<p>Here’s a snapshot showing the resulting output in the console.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1740504815204/45ce101a-4e8c-475b-ad4d-783b0940710b.png" alt="45ce101a-4e8c-475b-ad4d-783b0940710b" class="image--center mx-auto" width="460" height="203" loading="lazy"></p>
<h2 id="heading-how-to-render-the-user-interface-ui-and-handle-errors-in-react">How to Render the User Interface (UI) and Handle Errors in React</h2>
<p>In this section, you’ll learn how to render the UI based on our validated data and implement the error-handling mechanism using React states.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { z } <span class="hljs-keyword">from</span> <span class="hljs-string">"zod"</span>;
<span class="hljs-keyword">import</span> { useEffect, useState } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;

<span class="hljs-keyword">const</span> postSchema = z.object({
  userId: z.number().positive().int(),
  id: z.number().positive().int(),
  title: z.string(),
  body: z.string(),
});

<span class="hljs-keyword">const</span> postSchemaArray = z.array(postSchema); <span class="hljs-comment">// schema for an array of posts</span>

<span class="hljs-keyword">type</span> Posts = z.infer&lt;<span class="hljs-keyword">typeof</span> postSchemaArray&gt;; <span class="hljs-comment">// type of the posts</span>

<span class="hljs-keyword">const</span> ZodApi = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> [posts, setPosts] = useState&lt;Posts&gt;([]); <span class="hljs-comment">// State to store validated posts</span>
  <span class="hljs-keyword">const</span> [error, setError] = useState(<span class="hljs-string">""</span>); <span class="hljs-comment">// State to store any errors</span>
  useEffect(<span class="hljs-function">() =&gt;</span> {
    fetch(<span class="hljs-string">"https://jsonplaceholder.typicode.com/posts"</span>)
      .then(<span class="hljs-function">(<span class="hljs-params">response</span>) =&gt;</span> response.json())
      .then(<span class="hljs-function">(<span class="hljs-params">posts: Posts</span>) =&gt;</span> {
        <span class="hljs-keyword">const</span> validatedPosts = postSchemaArray.safeParse(posts); <span class="hljs-comment">// remember to use safeParse instead of parse</span>

        <span class="hljs-keyword">if</span> (validatedPosts.success === <span class="hljs-literal">false</span>) {
          <span class="hljs-built_in">console</span>.log(validatedPosts.error.name);
          setError(validatedPosts.error.message); <span class="hljs-comment">// set error state</span>
          <span class="hljs-keyword">return</span>;
        }

        <span class="hljs-comment">// we can now safely use the validatedPosts </span>
        <span class="hljs-built_in">console</span>.log(validatedPosts.data);
        setPosts(validatedPosts.data)
      });
  }, []);

  <span class="hljs-comment">// Handle loading state (optional)</span>
  <span class="hljs-keyword">if</span> (!posts.length &amp;&amp; !error) {
    <span class="hljs-keyword">return</span> &lt;div&gt;Loading posts...&lt;/div&gt;;
  }

  <span class="hljs-comment">// Handle error state</span>
  <span class="hljs-keyword">if</span> (error) {
    <span class="hljs-keyword">return</span> &lt;div&gt;<span class="hljs-built_in">Error</span> fetching Data&lt;<span class="hljs-regexp">/div&gt;; /</span><span class="hljs-regexp">/ Display user-friendly error message
  }

  return (
    &lt;div&gt;
      &lt;h1&gt;Posts&lt;/</span>h1&gt;
      &lt;ol&gt;
        {posts.map(<span class="hljs-function">(<span class="hljs-params">post</span>) =&gt;</span> (
          &lt;li key={post.id}&gt;
            {post.title}
          &lt;/li&gt;
        ))}
      &lt;/ol&gt;
    &lt;/div&gt;
  );
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> ZodApi;
</code></pre>
<p>In the code above, we’ve updated the <code>ZodApi</code> component to perform the following tasks:</p>
<ul>
<li><p>State declaration: The <code>posts</code> and <code>error</code> states hold the data and error (if any) gotten from the fetch request.</p>
</li>
<li><p>Error handling: We use the <code>posts</code> and <code>error</code> state to show a “Loading posts…” message when the posts are being fetched and no error occurs, and display an error message when an error occurs.</p>
</li>
<li><p>Rendering posts: It maps through the fetched posts and renders them on the UI.</p>
</li>
</ul>
<p>Output:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1740506104138/f2afac53-e312-4a40-af01-500e0dd349f7.gif" alt="f2afac53-e312-4a40-af01-500e0dd349f7" class="image--center mx-auto" width="400" height="814" loading="lazy"></p>
<p>After fetching the results, you should see the 100 posts displayed on your screen. If you followed the steps correctly, you'll find all 100 posts visible. If you encounter any issues, make sure the fetching process was successful.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>By incorporating Zod into your React development workflow, you can build more robust and reliable applications.</p>
<p>Zod empowers you to catch mismatched data early on, preventing errors and saving valuable debugging time. Also, the user-friendly error messages given by Zod validation enhance your application’s overall user experience.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Validate Forms with Zod and React-Hook-Form ]]>
                </title>
                <description>
                    <![CDATA[ Forms allow you to collect user data on your websites and apps. And validation is essential to guarantee type safety and the proper format for collected data. You can perform validation on both the client and server side of the application.  This is ... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/react-form-validation-zod-react-hook-form/</link>
                <guid isPermaLink="false">66bb580c0da5b03e481107d0</guid>
                
                    <category>
                        <![CDATA[ forms ]]>
                    </category>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                    <category>
                        <![CDATA[ zod ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Gift Uhiene ]]>
                </dc:creator>
                <pubDate>Wed, 17 Jan 2024 21:58:56 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2024/01/Frame-7.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Forms allow you to collect user data on your websites and apps. And validation is essential to guarantee type safety and the proper format for collected data. You can perform validation on both the client and server side of the application. </p>
<p>This is where Zod and React-Hook-Form come in as a dynamic duo, ready to take your forms to the next level.</p>
<p><a target="_blank" href="https://zod.dev/">Zod</a> is a validation library that provides a concise and expressive syntax for defining data schemas, making it an excellent choice for validating form data. </p>
<p>On the other hand, <a target="_blank" href="https://react-hook-form.com/">React-Hook-Form</a> is a lightweight form library for React that embraces uncontrolled components and simplifies form-building with its intuitive hooks-based API.</p>
<p>In this tutorial, you will learn how to build a type-safe form using React-Hook-Form for form management and Zod for both client-side and server-side validation.</p>
<h3 id="heading-heres-what-well-cover">Here's what we'll cover:</h3>
<ol>
<li><a class="post-section-overview" href="#heading-getting-started">Getting Started</a></li>
<li><a class="post-section-overview" href="#heading-how-to-define-form-types">How to Define Form Types</a></li>
<li><a class="post-section-overview" href="#heading-how-to-create-a-form-with-react-hook-form">How to Create a Form with react-hook-form</a></li>
<li><a class="post-section-overview" href="#heading-how-to-integrate-zod-for-schema-validation">How to Integrate Zod for Schema Validation</a></li>
<li><a class="post-section-overview" href="#heading-how-to-handle-server-side-errors">How to Handle Server-Side Errors</a></li>
<li><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></li>
</ol>
<h2 id="heading-getting-started">Getting Started</h2>
<p>To get started, clone the starter boilerplate for the project. Open up your terminal and run this command:</p>
<pre><code class="lang-bash">git <span class="hljs-built_in">clone</span> --branch starter https://github.com/Giftea/zod-rhf-fcc.git
</code></pre>
<p>You can find the final version on GitHub <a target="_blank" href="https://github.com/Giftea/zod-rhf-fcc">here</a>.</p>
<p>Once you've got the boilerplate on your local machine, run the following commands to install dependencies and start the project:</p>
<pre><code class="lang-bash">npm install
npm run dev
</code></pre>
<p>Point your browser to <a target="_blank" href="http://localhost:3000">http://localhost:3000</a>, and you'll be greeted by the starting page of our project.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/01/Screenshot-2024-01-16-at-15.21.10.png" alt="Image" width="600" height="400" loading="lazy">
<em>localhost</em></p>
<h2 id="heading-how-to-define-form-types">How to Define Form Types</h2>
<p>The <code>/types.ts</code> file will contain the types and schemas related to our form fields and their validation. Update the <code>/types.ts</code> file with the code below:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { FieldError, UseFormRegister } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-hook-form"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">type</span> FormData = {
    email: <span class="hljs-built_in">string</span>;
    githubUrl: <span class="hljs-built_in">string</span>;
    yearsOfExperience: <span class="hljs-built_in">number</span>;
    password: <span class="hljs-built_in">string</span>;
    confirmPassword: <span class="hljs-built_in">string</span>;
  };

  <span class="hljs-keyword">export</span> <span class="hljs-keyword">type</span> FormFieldProps = {
    <span class="hljs-keyword">type</span>: <span class="hljs-built_in">string</span>;
    placeholder: <span class="hljs-built_in">string</span>;
    name: ValidFieldNames;
    register: UseFormRegister&lt;FormData&gt;;
    error: FieldError | <span class="hljs-literal">undefined</span>;
    valueAsNumber?: <span class="hljs-built_in">boolean</span>;
  };


  <span class="hljs-keyword">export</span> <span class="hljs-keyword">type</span> ValidFieldNames =
  | <span class="hljs-string">"email"</span>
  | <span class="hljs-string">"githubUrl"</span>
  | <span class="hljs-string">"yearsOfExperience"</span>
  | <span class="hljs-string">"password"</span>
  | <span class="hljs-string">"confirmPassword"</span>;
</code></pre>
<p><code>FormData</code> represents the structure of the data expected in the form.</p>
<p><code>FormFieldProps</code> defines the properties expected by the form field component (which we will build later on). It includes:</p>
<ul>
<li><code>type</code>: The type of the input field (for example, text, password).</li>
<li><code>placeholder</code>: Placeholder text for the input field.</li>
<li><code>name</code>: The name of the field, which corresponds to one of the valid field names defined in the <code>ValidFieldNames</code> type.</li>
<li><code>register</code>: A function from <code>react-hook-form</code> (<code>UseFormRegister&lt;FormData&gt;</code>) used to register the input field with the form.</li>
<li><code>error</code>: Represents any validation error associated with the field. It can be <code>undefined</code> if there are no errors.</li>
<li><code>valueAsNumber</code> (optional): A boolean flag indicating whether the field value should be treated as a number. Defaults to <code>undefined</code>.</li>
</ul>
<p><code>ValidFieldNames</code> is a union type that enumerates the valid field names for the form. These correspond to the fields defined in the <code>FormData</code> type.</p>
<h2 id="heading-how-to-create-a-form-with-react-hook-form">How to Create a Form with React-Hook-Form</h2>
<p>Now that we have defined the types for the form, let's create a reusable form field component and the form component.</p>
<h3 id="heading-create-a-reusable-form-field-component">Create a Reusable Form Field Component</h3>
<p>Let's create a reusable <code>FormField</code> component that handles rendering an input element, registering it with the form using <code>react-hook-form</code>, and displaying a validation error message when necessary.</p>
<p>Head on to the <code>/app/components/FormField.tsx</code> file and update the component:</p>
<pre><code class="lang-tsx">import { FormFieldProps } from "@/types";

const FormField: React.FC&lt;FormFieldProps&gt; = ({
  type,
  placeholder,
  name,
  register,
  error,
  valueAsNumber,
}) =&gt; (
  &lt;&gt;
    &lt;input
      type={type}
      placeholder={placeholder}
      {...register(name, { valueAsNumber })}
    /&gt;
    {error &amp;&amp; &lt;span className="error-message"&gt;{error.message}&lt;/span&gt;}
  &lt;/&gt;
);
export default FormField;
</code></pre>
<h4 id="heading-imports">Imports:</h4>
<ul>
<li>The component imports the <code>FormFieldProps</code> type from the <code>@/types</code> module. This type contains the expected properties for a form field, such as <code>type</code>, <code>placeholder</code>, <code>name</code>, <code>register</code>, <code>error</code>, and <code>valueAsNumber</code>.</li>
</ul>
<h4 id="heading-input-element">Input Element:</h4>
<ul>
<li>The component renders an <code>&lt;input&gt;</code> element with attributes set based on the provided props (<code>type</code>, <code>placeholder</code>, <code>name</code>). </li>
<li>The <code>...register(name, { valueAsNumber })</code> syntax is used to register the input field with the form, enabling form state management.</li>
</ul>
<h4 id="heading-error-handling">Error Handling:</h4>
<ul>
<li>If there is a validation error, a <code>&lt;span&gt;</code> element is rendered, displaying the error message.</li>
</ul>
<h3 id="heading-create-the-form-component">Create the Form Component</h3>
<p>The <code>Form</code> component will utilize the <code>react-hook-form</code> library to manage the form state. It modularizes form fields by using our reusable <code>FormField</code> component.</p>
<p>Navigate to <code>app/components/Form.tsx</code> and update it with the code below:</p>
<pre><code class="lang-tsx">import { useForm } from "react-hook-form";
import { FormData } from "@/types";
import FormField from "./FormField";

function Form() {
  const {
    register,
    handleSubmit,
    formState: { errors },
    setError,
  } = useForm&lt;FormData&gt;();

  const onSubmit = async (data: FormData) =&gt; {
      console.log("SUCCESS", data);
  }

  return (
      &lt;form onSubmit={handleSubmit(onSubmit)}&gt;
        &lt;div className="grid col-auto"&gt;
          &lt;h1 className="text-3xl font-bold mb-4"&gt;
            Zod &amp; React-Hook-Form
          &lt;/h1&gt;
          &lt;FormField
            type="email"
            placeholder="Email"
            name="email"
            register={register}
            error={errors.email}
          /&gt;

          &lt;FormField
            type="text"
            placeholder="GitHub URL"
            name="githubUrl"
            register={register}
            error={errors.githubUrl}
          /&gt;

          &lt;FormField
            type="number"
            placeholder="Years of Experience (1 - 10)"
            name="yearsOfExperience"
            register={register}
            error={errors.yearsOfExperience}
            valueAsNumber
          /&gt;

          &lt;FormField
            type="password"
            placeholder="Password"
            name="password"
            register={register}
            error={errors.password}
          /&gt;

          &lt;FormField
            type="password"
            placeholder="Confirm Password"
            name="confirmPassword"
            register={register}
            error={errors.confirmPassword}
          /&gt;
          &lt;button type="submit" className="submit-button"&gt;
            Submit
          &lt;/button&gt;
        &lt;/div&gt;
      &lt;/form&gt;
  );
}

export default Form;
</code></pre>
<h4 id="heading-imports-1">Imports:</h4>
<ul>
<li>The <code>useForm</code> hook provides functionality for managing form state and validation.</li>
<li><code>FormData</code> represents the structure of the form data.</li>
<li><code>FormField</code> is our reusable form field component.</li>
</ul>
<h4 id="heading-form-component">Form Component:</h4>
<ul>
<li>Form-related functions and state variables are destructured from the <code>useForm</code> hook, which is explicitly typed with <code>FormData</code> to define the shape of the form data.</li>
<li>Within the form, the <code>FormField</code> components are rendered for different input fields such as email, GitHub URL, years of experience, password, and confirm password.</li>
</ul>
<h4 id="heading-run-code">Run Code:</h4>
<p>Import the <code>Form</code> component into <code>/app/page.tsx</code> file:</p>
<pre><code class="lang-tsx">"use client";
import Form from "./components/Form";

function Home() {

  return (
    &lt;main className="flex min-h-screen flex-col items-center justify-between p-24"&gt;
     &lt;Form /&gt;
    &lt;/main&gt;
  );
}

export default Home;
</code></pre>
<p>Visit <a target="_blank" href="http://localhost:3000/">http://localhost:3000/</a> to view the form:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/01/Screenshot-2024-01-11-at-11.40.22.png" alt="Image" width="600" height="400" loading="lazy">
<em><a target="_blank" href="http://localhost:3000/">http://localhost:3000/</a></em></p>
<p>In summary, our <code>Form</code> component is a basic form structure that uses the <code>react-hook-form</code> library for state management and employs a reusable <code>FormField</code> component to handle the rendering and validation of individual form fields. </p>
<h2 id="heading-how-to-integrate-zod-for-schema-validation">How to Integrate Zod for Schema Validation</h2>
<p>Zod stands out as a schema declaration and validation library, with TypeScript as its primary focus. The term "schema" encompasses various data types, ranging from strings, numbers, and booleans to more complex objects.</p>
<h3 id="heading-define-a-form-schema-with-zod">Define a Form Schema with Zod</h3>
<p>Let's create a TypeScript-backed form schema using Zod for our form structure.</p>
<p>Head to your <code>/types.ts</code> file, add the new imports, and create a user schema with the code below:</p>
<pre><code class="lang-ts"> <span class="hljs-keyword">import</span> { z, ZodType } <span class="hljs-keyword">from</span> <span class="hljs-string">"zod"</span>; <span class="hljs-comment">// Add new import</span>

 <span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> UserSchema: ZodType&lt;FormData&gt; = z
  .object({
    email: z.string().email(),
    githubUrl: z
      .string()
      .url()
      .includes(<span class="hljs-string">"github.com"</span>, { message: <span class="hljs-string">"Invalid GitHub URL"</span> }),
    yearsOfExperience: z
      .number({
        required_error: <span class="hljs-string">"required field"</span>,
        invalid_type_error: <span class="hljs-string">"Years of Experience is required"</span>,
      })
      .min(<span class="hljs-number">1</span>)
      .max(<span class="hljs-number">10</span>),
    password: z
      .string()
      .min(<span class="hljs-number">8</span>, { message: <span class="hljs-string">"Password is too short"</span> })
      .max(<span class="hljs-number">20</span>, { message: <span class="hljs-string">"Password is too long"</span> }),
    confirmPassword: z.string(),
  })
  .refine(<span class="hljs-function">(<span class="hljs-params">data</span>) =&gt;</span> data.password === data.confirmPassword, {
    message: <span class="hljs-string">"Passwords do not match"</span>,
    path: [<span class="hljs-string">"confirmPassword"</span>], <span class="hljs-comment">// path of error</span>
  });
</code></pre>
<h4 id="heading-imports-2">Imports:</h4>
<ul>
<li><code>z</code> is an instance of the Zod object. </li>
<li><code>ZodType</code> is a generic type that represents a Zod schema type for a specific data structure.</li>
</ul>
<h4 id="heading-user-schema">User Schema:</h4>
<ul>
<li><code>export const UserSchema: ZodType&lt;FormData&gt; = ...</code>: The <code>UserSchema</code> represents a Zod type that corresponds to the structure defined by the <code>FormData</code> type.</li>
<li><code>z.object({...})</code>: This part defines an object schema using Zod. The object has several fields, each with its own validation rules.</li>
<li>Inside the object, each field is defined with its own validation rules using Zod methods like <code>z.string()</code>, <code>z.url()</code>, <code>z.number()</code>, and <code>z.min()</code>. Optional custom error messages are provided for some of the fields.</li>
<li><code>z.refine((data) =&gt; data.password === data.confirmPassword, { /* ... */ });</code>: Adds a refinement to the schema to check if the <code>password</code> and <code>confirmPassword</code> fields match. If not, a custom error message is provided, and the error is associated with the <code>confirmPassword</code> field.</li>
</ul>
<h3 id="heading-how-to-integrate-zod-with-react-hook-form-for-validation">How to Integrate Zod with React-Hook-Form for validation</h3>
<p>Now that we've set up the Zod schema for the form, let's integrate it with our existing Form component. To do this, we'll use <code>zodResolver</code> from the <code>[@hookform](https://www.npmjs.com/package/@hookform/resolvers)</code> library.</p>
<p><code>zodResolver</code> is a resolver function that integrates the Zod schema validation with the form validation process. </p>
<p>Head over to the <code>app/components/Form.tsx</code> file and update it with the code below:</p>
<pre><code class="lang-tsx">// Update imports
import { FormData, UserSchema } from "@/types";
import { zodResolver } from "@hookform/resolvers/zod";

function Form() {
  const {
    register,
    handleSubmit,
    formState: { errors },
    setError,
  } = useForm&lt;FormData&gt;({
    resolver: zodResolver(UserSchema), // Apply the zodResolver
  });

{/* Existing Code...*/}

}
</code></pre>
<p>If you try submitting the form with empty input fields, you will see error messages on the browser.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/01/Screenshot-2024-01-11-at-20.38.03.png" alt="Image" width="600" height="400" loading="lazy">
<em>Error Messages - http://localhost:3000/</em></p>
<p>Additionally, our custom error messages, such as prompting users to provide a valid GitHub URL and checking if the passwords match, are demonstrated in the image below:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/01/Screenshot-2024-01-11-at-20.59.18.png" alt="Image" width="600" height="400" loading="lazy">
<em>Custom Error Messages - http://localhost:3000/</em></p>
<h2 id="heading-how-to-handle-server-side-errors">How to Handle Server-Side Errors</h2>
<p>When creating forms, data integrity and type safety are very important, given that submitted data goes to the website's server. This leads us to the significance of handling server-side errors — an extra security measure to make sure data from the client is accurate and non-malicious.</p>
<h3 id="heading-how-to-implement-server-side-validation">How to Implement Server-Side Validation</h3>
<p>To implement server-side validation, we will leverage Next.js' backend capabilities to build a simple server. This server will receive and validate the data submitted through our form.</p>
<p>Navigate to <code>app/api/form/route.ts</code> and include the code below:</p>
<pre><code class="lang-ts"><span class="hljs-keyword">import</span> { UserSchema } <span class="hljs-keyword">from</span> <span class="hljs-string">"@/types"</span>;
<span class="hljs-keyword">import</span> { NextResponse } <span class="hljs-keyword">from</span> <span class="hljs-string">"next/server"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">POST</span>(<span class="hljs-params">request: Request</span>) </span>{
  <span class="hljs-comment">// Retrieve the JSON data from the request body</span>
  <span class="hljs-keyword">const</span> body = <span class="hljs-keyword">await</span> request.json();

  <span class="hljs-comment">// Use Zod to validate the received data against the UserSchema</span>
  <span class="hljs-keyword">const</span> result = UserSchema.safeParse(body);

  <span class="hljs-comment">// Check if the validation is successful</span>
  <span class="hljs-keyword">if</span> (result.success) {
    <span class="hljs-keyword">return</span> NextResponse.json({ success: <span class="hljs-literal">true</span> });
  }

  <span class="hljs-comment">// If validation errors, map them into an object</span>
  <span class="hljs-keyword">const</span> serverErrors = <span class="hljs-built_in">Object</span>.fromEntries(
    result.error?.issues?.map(<span class="hljs-function">(<span class="hljs-params">issue</span>) =&gt;</span> [issue.path[<span class="hljs-number">0</span>], issue.message]) || []
  );

  <span class="hljs-comment">// Respond with a JSON object containing the validation errors</span>
  <span class="hljs-keyword">return</span> NextResponse.json({ errors: serverErrors });
}
</code></pre>
<h4 id="heading-imports-3">Imports:</h4>
<ul>
<li>The <code>UserSchema</code> we defined earlier is imported.</li>
<li><code>NextResponse</code> from the <code>next/server</code> module, which allows us to craft server responses in a Next.js environment.</li>
</ul>
<h4 id="heading-post-function">POST Function:</h4>
<ul>
<li><code>const body = await request.json()</code>: Retrieves the JSON data from the request body and stores it in the <code>body</code> variable.</li>
<li><code>const result = UserSchema.safeParse(body)</code>: Utilizes the <code>safeParse</code> method provided by Zod to validate the received data against the <code>UserSchema</code>. The result contains information about whether the validation was successful and, if not, details about the validation issues.</li>
<li><code>if (result.success) { return NextResponse.json({ success: true }); }</code>: If the validation is successful, a JSON response with <code>{ success: true }</code> is sent.</li>
<li><code>const serverErrors = Object.fromEntries(/* ... */)</code>: If there are validation errors, the code maps them into an object with field names and corresponding error messages.</li>
<li><code>return NextResponse.json({ errors: serverErrors })</code>: Responds with a JSON object containing the validation errors.</li>
</ul>
<p>In your terminal, stop running the project and run <code>npm run dev</code> again to restart the server.</p>
<h3 id="heading-how-to-integrate-server-side-validation">How to Integrate Server-Side Validation</h3>
<p>To integrate the server-side validation, we have to update the <code>onSubmit</code> function in the Form component.</p>
<p>Head over to the <code>/app/components/Form.tsx</code> file and update the imports and <code>onSubmit</code> function:</p>
<pre><code class="lang-tsx">// Update import
import { FormData, UserSchema, ValidFieldNames } from "@/types";  
import axios from "axios";

function Form() {
{/* Existing Code... */}

  const onSubmit = async (data: FormData) =&gt; {
    try {
      const response = await axios.post("/api/form", data); // Make a POST request
      const { errors = {} } = response.data; // Destructure the 'errors' property from the response data

      // Define a mapping between server-side field names and their corresponding client-side names
      const fieldErrorMapping: Record&lt;string, ValidFieldNames&gt; = {
        email: "email",
        githubUrl: "githubUrl",
        yearsOfExperience: "yearsOfExperience",
        password: "password",
        confirmPassword: "confirmPassword",
      };

      // Find the first field with an error in the response data
      const fieldWithError = Object.keys(fieldErrorMapping).find(
        (field) =&gt; errors[field]
      );

      // If a field with an error is found, update the form error state using setError
      if (fieldWithError) {
        // Use the ValidFieldNames type to ensure the correct field names
        setError(fieldErrorMapping[fieldWithError], {
          type: "server",
          message: errors[fieldWithError],
        });
      }
    } catch (error) {
      alert("Submitting form failed!");
    }
  };
{/* Existing Code... */}
}
</code></pre>
<ul>
<li><code>axios</code> is used to make a POST request to the server endpoint <code>/api/form</code> with the form data.</li>
<li>The <code>errors</code> object is extracted from the response data.</li>
<li>A mapping (<code>fieldErrorMapping</code>) between field names and their corresponding <code>ValidFieldNames</code> is defined.</li>
<li>It then checks if there are any errors related to form fields by iterating over the <code>fieldErrorMapping</code> and finding the first field with an error.</li>
<li>If a field with an error is found, the <code>setError</code> function from <code>react-hook-form</code> is used to set an error for the corresponding field. The error type is marked as "server," and the error message comes from the server response.</li>
<li>If there's an error in the entire try block, it catches the error and displays an alert: "Submitting form failed!"</li>
</ul>
<p>Now, to test if we can receive errors from the server, we'll deliberately send improperly formatted data to the server. In your <code>onSubmit</code> function, replace the <code>data</code> object with the incorrect data in the code block below:</p>
<pre><code class="lang-tsx">
{/* Existing Code...*/}
  const onSubmit = async (data: FormData) =&gt; {

    try {
      // Update data sent in axios with incorrect data
      const response = await axios.post("/api/form", {
        email: "Not an email",
        githubUrl: "Not a URL",
        yearsOfExperience: "Hello",
        password: 1234,
        confirmPassword: 1234,
      }); // Make a POST request

{/* Existing Code...*/}
}
</code></pre>
<p>Fill the form in the browser normally and submit the form. </p>
<p>Inspect the "Network" tab within the browser's developer tools. You'll find error messages coming directly from the server, as demonstrated in the image below:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/01/Screenshot-2024-01-12-at-10.21.47.png" alt="Image" width="600" height="400" loading="lazy">
<em>Server errors - http://localhost:3000/</em></p>
<p>If you're not getting any response from your server, remember to stop running your project in your terminal and run <code>npm run dev</code> again to re-start the server.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this tutorial, we built a form with React-Hook-Form and validated it with Zod. With Zod, we explored schema validation, customized error messages, and server-side errors. The integration of React-Hook-Form and Zod presents a powerful, developer-friendly solution to creating resilient forms.</p>
<p>You can reach out to me on <a target="_blank" href="https://twitter.com/dev_giftea">Twitter</a> if you have any questions.</p>
<p>You can check out the <a target="_blank" href="https://github.com/Giftea/zod-rhf-fcc">source code</a> and the deployed <a target="_blank" href="https://zod-rhf-fcc.vercel.app/">app</a>.</p>
<h3 id="heading-resources">Resources:</h3>
<ul>
<li><a target="_blank" href="https://zod.dev/">Zod Documentation</a></li>
<li><a target="_blank" href="https://zod.dev/ERROR_HANDLING?id=error-handling-in-zod">Zod Error Handling</a></li>
<li><a target="_blank" href="https://react-hook-form.com/get-started">React-Hook-Form Documentation</a></li>
<li><a target="_blank" href="https://www.npmjs.com/package/@hookform/resolvers">Hookform Resolvers</a></li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
