<?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[ form validation - 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[ form validation - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Thu, 27 Aug 2026 01:03:52 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/form-validation/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Handle Forms in Next.js with Server Actions and Zod for Validation ]]>
                </title>
                <description>
                    <![CDATA[ Forms are essential in modern websites, as they help you collect your users’ information. So knowing how to handle forms properly is crucial when you’re building web applications. In this article, you will learn how to handle forms in Next.js using s... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/handling-forms-nextjs-server-actions-zod/</link>
                <guid isPermaLink="false">6740b5ae31fc8f5b09184849</guid>
                
                    <category>
                        <![CDATA[ Next.js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Frontend Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ form validation ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Chidera Humphrey ]]>
                </dc:creator>
                <pubDate>Fri, 22 Nov 2024 16:47:42 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1732137561737/293681e0-d2f4-4d88-9fbe-f7e5e9113554.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Forms are essential in modern websites, as they help you collect your users’ information. So knowing how to handle forms properly is crucial when you’re building web applications.</p>
<p>In this article, you will learn how to handle forms in Next.js using server actions and zod.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#introduction-to-server-actions-in-nextjs">Introduction to Server Actions in Next.js</a></p>
</li>
<li><p><a class="post-section-overview" href="#introduction-to-zod-for-validation">Introduction to zod for Validation</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-build-the-contact-form-component">How to Build the Contact Form Component</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-create-the-server-actions-and-validate-the-form-data-with-zod">How to Create the Server Actions and Validate the Form Data with zod</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-integrate-the-server-action-into-our-contact-form">How to Integrate the Server Action into Our Contact Form</a></p>
</li>
<li><p><a class="post-section-overview" href="#conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-prerequisites-and-setting-up-the-project">Prerequisites and Setting Up the Project</h2>
<p>For this tutorial, I assume that you know JavaScript and how to set up a Next.js project (I'm not going to walk through that set up here).</p>
<p>If you haven’t yet set up your Next.js project, use the following command and follow the prompts:</p>
<pre><code class="lang-sh">npx create-next-app
</code></pre>
<p>This is what we are going to build in this tutorial:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1732051133068/960bd90a-cb22-4e8b-86e2-fdb78b5cf330.gif" alt="working form" class="image--center mx-auto" width="400" height="212" loading="lazy"></p>
<p><strong>Note</strong>: this tutorial mainly focuses on the logic and not the design. For the complete design, you can visit the GitHub repository which I’ve linked to at the end.</p>
<h2 id="heading-introduction-to-server-actions-in-nextjs">Introduction to Server Actions in Next.js</h2>
<p>So what are server actions? Server actions are pretty much what they sound like—actions or functions that run on the server. With server actions, you can make calls to external APIs or fetch data from a database.</p>
<p>Prior to Next.js 13, you had to use routes to handle API calls and form submissions. This was complex and cumbersome.</p>
<p>But the introduction of server actions lets you communicate with external APIs and databases directly in your Next.js components.</p>
<p>By running on the server, server actions enable secure handling of data processing, mitigating security risks.</p>
<p>Server actions are also useful in handling forms as they let you communicate directly with your server and limit the exposure of important credentials to the client.</p>
<p>There are two ways to create server actions:</p>
<ul>
<li>The first method is using the <code>"use server"</code> directive at the top level of a function. You can only use this method inside a server component. Using it inside a client component will result in an error.</li>
</ul>
<p>For example:</p>
<pre><code class="lang-ts"><span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getPosts</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-string">"use server"</span>; <span class="hljs-comment">// this makes getPosts a server actions</span>

  <span class="hljs-comment">// rest of code</span>
}
</code></pre>
<ul>
<li>The other method is to create a separate file and add <strong>"use server"</strong> at the top of the file. This ensures that any async function exported from the file is a server action.</li>
</ul>
<pre><code class="lang-ts"><span class="hljs-comment">// action.ts</span>

<span class="hljs-string">"use 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">getPosts</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">"https:..."</span>);
  <span class="hljs-keyword">const</span> data = res.json();

  <span class="hljs-keyword">return</span> data;
}
</code></pre>
<p>In the code example above, <code>getPosts</code> is a server action.</p>
<h2 id="heading-introduction-to-zod-for-validation">Introduction to Zod for Validation</h2>
<p>Zod is a validation library that you can use to validate form entries on the server side. This ensures consistency across both the client and server.</p>
<p>Zod is a TypeScript-first library, which means that it comes with type safety out of the box.</p>
<p>To install Zod in your Next.js application, use the following command:</p>
<pre><code class="lang-sh">npm install zod
</code></pre>
<p>At the core of the Zod library are schemas. You can use schemas to validate inputs.</p>
<p>Here's how to define a schema:</p>
<pre><code class="lang-ts"><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> contactSchema = z.object({
  name: z.string().min(<span class="hljs-number">2</span>, { message: <span class="hljs-string">"Name must be at least 2 characters"</span> }),
  email: z.string().email({ message: <span class="hljs-string">"Invalid email address"</span> }),
  message: z
    .string()
    .min(<span class="hljs-number">10</span>, { message: <span class="hljs-string">"Message must be at least 10 characters"</span> }),
});
</code></pre>
<p>Inside the <code>contactSchema</code>, we are specifying that:</p>
<ul>
<li><p><code>name</code> is of type <code>string</code> and should be a minimum of 2 characters,</p>
</li>
<li><p><code>email</code> is of type <code>string</code> and <code>email</code>, and</p>
</li>
<li><p><code>message</code> is of type <code>string</code> and should be a minimum of 10 characters.</p>
</li>
</ul>
<p>The <code>message</code> property is what will be displayed on the screen when all or any of the validation fails.</p>
<p>In the next section, we are going to build the contact form.</p>
<h2 id="heading-how-to-build-the-contact-form-component">How to Build the Contact Form Component</h2>
<p>In this section, we are going to build the UI of the contact form.</p>
<p>Inside the <code>app</code> directory, create a folder called "components.<strong>"</strong></p>
<p>Inside of the <code>components</code> folder, create a new file, <code>contactForm.tsx</code>, and add the following code:</p>
<pre><code class="lang-typescript"><span class="hljs-string">"use client"</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">ContactForm</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> (
    &lt;form action=<span class="hljs-string">""</span>&gt;
      &lt;input <span class="hljs-keyword">type</span>=<span class="hljs-string">"text"</span> name=<span class="hljs-string">"name"</span> placeholder=<span class="hljs-string">"Enter your name"</span> /&gt;
      &lt;input <span class="hljs-keyword">type</span>=<span class="hljs-string">"email"</span> name=<span class="hljs-string">"email"</span> placeholder=<span class="hljs-string">"Enter your email"</span> /&gt;
      &lt;textarea name=<span class="hljs-string">"message"</span> cols={<span class="hljs-number">30</span>} rows={<span class="hljs-number">10</span>} placeholder=<span class="hljs-string">"Type in your message"</span>&gt;&lt;/textarea&gt;
      &lt;button <span class="hljs-keyword">type</span>=<span class="hljs-string">"submit"</span>&gt;Send Message&lt;/button&gt;
    &lt;/form&gt;
  );
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> ContactForm;
</code></pre>
<p>In the code above, we are creating a simple contact form. We made it a client component – you’ll see why in a bit.</p>
<p>Import the <code>ContactForm</code> component in your <code>page.tsx</code> file:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> ContactForm <span class="hljs-keyword">from</span> <span class="hljs-string">"./components/contactForm.tsx"</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Home</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> (
    &lt;div&gt;
      &lt;h2&gt;Contact Form&lt;/h2&gt;
      &lt;ContactForm /&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<p>You should have something like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1732048786231/694ac568-9d71-4597-ba61-962483740320.png" alt="contact form image" class="image--center mx-auto" width="838" height="632" loading="lazy"></p>
<p>Next, we are going to validate our form data using zod.</p>
<h2 id="heading-how-to-create-the-server-actions-and-validate-the-form-data-with-zod">How to Create the Server Actions and Validate the Form Data with zod</h2>
<p>In this section, we are going to create our server action and validate our form entries with zod.</p>
<p>In the <strong>app</strong> folder, create another folder, <code>api</code>.</p>
<p>Inside the <code>api</code> folder, create a file called <code>action.ts</code> and paste in the following code:</p>
<pre><code class="lang-ts"><span class="hljs-string">"use server"</span>;

<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> contactFormSchema = z.object({
  name: z.string().trim().min(<span class="hljs-number">1</span>, { message: <span class="hljs-string">"Name field is required"</span> }),
  email: z.string().email({ message: <span class="hljs-string">"Invalid email address"</span> }),
  message: z.string().trim().min(<span class="hljs-number">1</span>, { message: <span class="hljs-string">"Please type in a message"</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">sendEmail</span>(<span class="hljs-params">prevState: <span class="hljs-built_in">any</span>, formData: FormData</span>) </span>{
  <span class="hljs-keyword">const</span> contactFormData = <span class="hljs-built_in">Object</span>.fromEntries(formData);
  <span class="hljs-keyword">const</span> validatedContactFormData = contactFormSchema.safeParse(contactFormData);


  <span class="hljs-keyword">if</span> (!validatedContactFormData.success) {
    <span class="hljs-keyword">const</span> formFieldErrors =
      validatedContactFormData.error.flatten().fieldErrors;

    <span class="hljs-keyword">return</span> {
      errors: {
        name: formFieldErrors?.name,
        email: formFieldErrors?.email,
        message: formFieldErrors?.message,
      },
    };
  }

  <span class="hljs-keyword">return</span> {
    success: <span class="hljs-string">"Your message was sent successfully!"</span>,
  };
}
</code></pre>
<p>In the code above, we defined a <code>contactFormSchema</code> for validating our form entries.</p>
<p>The <code>sendEmail</code> function (which is our server action) accepts two arguments:</p>
<ul>
<li><p><code>prevState</code> which will be used in to display our error and success messages, and</p>
</li>
<li><p><code>formData</code> which is the entries from our form</p>
</li>
</ul>
<p>FormData makes it possible for our function to have access to the form fields without using <code>useState</code> and it relies on the <code>name</code> attribute.</p>
<p>We are using <code>Object.fromEntries()</code> to convert the raw <code>formData</code> into a regular JavaScript object and we’re storing it in the <code>contactFormData</code> variable.</p>
<p>Next, we are validating the <code>contactFormData</code> using the <code>safeParse()</code> method of our zod schema, <code>contactFormSchema</code>.</p>
<p>As a good programming practice, we return early by checking if the validation fails. If the validation fails, we return an object with an <code>error</code> property, which is an object containing the error message of each form field.</p>
<p><code>formFieldsError</code> is assigned the value of the error object from zod, which contains the error message of each form field.</p>
<p>If everything goes well, we simply return an object with a <code>success</code> property.</p>
<p><strong>Note:</strong> this is where you send the message to your email using any email service provider of your choice. For the sake of the article, we are simply returning an object.</p>
<p>In the next section, we are going to integrate the server action in our contact form.</p>
<h2 id="heading-how-to-integrate-the-server-action-into-our-contact-form">How to Integrate the Server Action into Our Contact Form</h2>
<p>In this section, we are going to integrate the server action into our contact form.</p>
<p>Navigate to the <code>contactForm.tsx</code> file and replace the content with the following code:</p>
<pre><code class="lang-typescript"><span class="hljs-string">"use client"</span>;

<span class="hljs-keyword">import</span> { useFormState, useFormStatus } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-dom"</span>;
<span class="hljs-keyword">import</span> { sendEmail } <span class="hljs-keyword">from</span> <span class="hljs-string">"../api/action"</span>;

<span class="hljs-keyword">const</span> initialState = {
  success: <span class="hljs-string">""</span>,
  errors: {
    name: <span class="hljs-string">""</span>,
    email: <span class="hljs-string">""</span>,
    message: <span class="hljs-string">""</span>,
  }
};

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">ContactForm</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> [state, formAction] = useFormState(sendEmail, initialState);

  <span class="hljs-keyword">return</span> (
    &lt;div&gt;
      &lt;div className=<span class="hljs-string">"py-6"</span>&gt;
        &lt;form action={formAction}&gt;
          &lt;div className=<span class="hljs-string">"mb-4"</span>&gt;
            &lt;label htmlFor=<span class="hljs-string">"name"</span>&gt;Your name&lt;/label&gt;
            &lt;br /&gt;
            &lt;input
              <span class="hljs-keyword">type</span>=<span class="hljs-string">"text"</span>
              name=<span class="hljs-string">"name"</span>
              id=<span class="hljs-string">"name"</span>
              <span class="hljs-comment">// required</span>
              className=<span class="hljs-string">"border w-full md:w-3/4 py-2 pl-2 rounded-lg rounded-l-lg block md:inline focus:outline-slate-500 border-gray-500"</span>
              placeholder=<span class="hljs-string">"Enter your name..."</span>
            /&gt;
            {state.errors?.name &amp;&amp; (
              &lt;p className=<span class="hljs-string">"text-red-500"</span>&gt;{state.errors.name}&lt;/p&gt;
            )}
          &lt;/div&gt;
          &lt;div className=<span class="hljs-string">"mb-4"</span>&gt;
            &lt;label htmlFor=<span class="hljs-string">"email"</span>&gt;Your email&lt;/label&gt;
            &lt;br /&gt;
            &lt;input
              <span class="hljs-keyword">type</span>=<span class="hljs-string">"email"</span>
              name=<span class="hljs-string">"email"</span>
              id=<span class="hljs-string">"email"</span>
              <span class="hljs-comment">// required</span>
              className=<span class="hljs-string">"border w-full md:w-3/4 py-2 pl-2 rounded-lg rounded-l-lg block md:inline focus:outline-slate-500 border-gray-500"</span>
              placeholder=<span class="hljs-string">"Enter your email..."</span>
            /&gt;
            {state.errors?.email &amp;&amp; (
              &lt;p className=<span class="hljs-string">"text-red-500"</span>&gt;{state.errors.email}&lt;/p&gt;
            )}
          &lt;/div&gt;
          &lt;div&gt;
            &lt;label htmlFor=<span class="hljs-string">"message"</span>&gt;Message&lt;/label&gt;
            &lt;br /&gt;
            &lt;textarea
              name=<span class="hljs-string">"message"</span>
              id=<span class="hljs-string">"message"</span>
              <span class="hljs-comment">// required</span>
              cols={<span class="hljs-number">100</span>}
              rows={<span class="hljs-number">10</span>}
              className=<span class="hljs-string">"border w-full md:w-3/4 py-3 pl-2 rounded-lg focus:outline-slate-500 border-gray-500"</span>
              placeholder=<span class="hljs-string">"Enter your message..."</span>
            &gt;&lt;/textarea&gt;
            {state.errors?.message &amp;&amp; (
              &lt;p className=<span class="hljs-string">"text-red-500"</span>&gt;{state.errors.message}&lt;/p&gt;
            )}
          &lt;/div&gt;
          &lt;SubmitButton /&gt;
        &lt;/form&gt;
      &lt;/div&gt;
      {state?.success &amp;&amp; &lt;p className=<span class="hljs-string">"text-green-600"</span>&gt;{state.success}&lt;/p&gt;}
    &lt;/div&gt;
  );
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> ContactForm;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">SubmitButton</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> { pending } = useFormStatus();

  <span class="hljs-keyword">return</span> (
    &lt;button
      <span class="hljs-keyword">type</span>=<span class="hljs-string">"submit"</span>
      disabled={pending ? <span class="hljs-literal">true</span> : <span class="hljs-literal">false</span>}
      className=<span class="hljs-string">"bg-green-600 text-white font-semibold px-3 py-2 rounded-lg"</span>
    &gt;
      {pending ? (
        &lt;span&gt;
          Submitting &lt;RiLoader5Fill className=<span class="hljs-string">"animate-spin"</span> /&gt;
        &lt;/span&gt;
      ) : (
        <span class="hljs-string">"Submit"</span>
      )}
    &lt;/button&gt;
  );
}
</code></pre>
<p>In the updated code above, we imported two hooks: <code>useFormState</code> and <code>useFormStatus</code> from "react-dom" and <code>sendEmail</code> from "api/action.ts".</p>
<p>Next, we created a <code>initialState</code> variable to hold our initial state. This will be used in the <code>useFormState</code> hook.</p>
<p><code>initialState</code> is an object with:</p>
<ul>
<li><p>a <code>success</code> property for the success message of our server action, and</p>
</li>
<li><p>an <code>errors</code> object, which is equal to the <code>errors</code> object we return in our server action if the validation fails.</p>
</li>
</ul>
<p>Inside our <code>ContactForm</code> component, we are using the <code>useFormState</code> hook. This hook accepts two arguments: a server action and an initial state and returns an array with two values: current state and <code>formAction</code>.</p>
<p><code>formAction</code> will be passed into the <code>action</code> prop of the <strong>form</strong> element. This will handle the submission of our form, which incorporates the zod validation.</p>
<p>Below each form field, we conditionally render the error message of each of the form field respectively.</p>
<p>Below the <strong>form</strong> element, we render the success message if the form was successfully submitted.</p>
<p>The submit button is put into a different component, <code>SubmitButton</code> so we can make use of the <code>useFormStatus</code> hook.</p>
<p>The <code>useFormStatus</code> hook returns an object with a <code>pending</code> property, which we can use to disable the submit button when the form is submitted.</p>
<p>Assuming everything went correctly, you should have a working contact form like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1732051093066/c6cd1da7-fe24-4eea-85db-a6845efc501d.gif" alt="working form" class="image--center mx-auto" width="400" height="212" loading="lazy"></p>
<p>Congratulations! You have just created a contact form using server actions and the zod validation library.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this article, you learned what server actions are and how to use the zod library. You also used server actions and zod to build a contact form.</p>
<p>Server actions are not limited to form submission and can also be used for fetching data from external APIs and databases.</p>
<p>You can learn more with these resources:</p>
<ul>
<li><p><a target="_blank" href="https://zod.dev/">zod documentation</a></p>
</li>
<li><p><a target="_blank" href="https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations">server action documentation</a></p>
</li>
</ul>
<p>Here's the <a target="_blank" href="https://github.com/DeraCodings/server-action-zod">GitHub repository</a> of the complete project.</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
