DEV Community

Cover image for Building a Type-Safe Form in NextJs with Zod
Mohammad Ezzeddin Pratama
Mohammad Ezzeddin Pratama

Posted on

3 1 1 1 1

Building a Type-Safe Form in NextJs with Zod

Forms are a crucial part of any web application, and building them in a type-safe manner ensures fewer bugs and better maintainability. In this blog post, we'll create a complete form in React using TypeScript and Zod for validation. All the code will be contained in a single file for simplicity.

What is Zod?
Zod is a TypeScript-first schema declaration and validation library. It allows you to define schemas for your data, which can then be used to validate inputs in a type-safe manner. This makes Zod an excellent choice for validating forms in React applications.

Setting Up the Project
If you haven't already, you'll need to install React, TypeScript, and Zod in your project:

npm install react react-dom typescript zod
Enter fullscreen mode Exit fullscreen mode

Now, let's create a simple form that collects a user's first name, last name, and email, with validation for each field.

The Code
Here's the complete code for the form component:

typescript

import React, { useState } from 'react';
import { z } from 'zod';

// Define the schema using Zod
const formSchema = z.object({
  firstName: z.string().min(1, "First Name is required"),
  lastName: z.string().min(1, "Last Name is required"),
  email: z.string().email("Invalid email address"),
});

// TypeScript type derived from the Zod schema
type FormData = z.infer<typeof formSchema>;

export default function ZodForm() {
  // State to manage form input values
  const [formData, setFormData] = useState<FormData>({
    firstName: '',
    lastName: '',
    email: '',
  });

  // State to manage form errors
  const [formErrors, setFormErrors] = useState<Partial<FormData>>({});

  // Handle input changes
  const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value } = event.target;
    setFormData({ ...formData, [name]: value });
  };

  // Handle form submission
  const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();

    // Validate form data using Zod
    const result = formSchema.safeParse(formData);

    if (!result.success) {
      // If validation fails, set errors
      const errors: Partial<FormData> = {};
      result.error.errors.forEach((error) => {
        if (error.path[0]) {
          errors[error.path[0] as keyof FormData] = error.message;
        }
      });
      setFormErrors(errors);
    } else {
      // If validation succeeds, clear errors and handle form submission
      setFormErrors({});
      alert('Form submitted successfully!');
      console.log(result.data);
    }
  };

  return (
    <form onSubmit={handleSubmit} className="max-w-sm mx-auto mt-10">
      <div className="mb-4">
        <label htmlFor="firstName" className="block text-sm font-medium text-gray-700">
          First Name
        </label>
        <input
          type="text"
          id="firstName"
          name="firstName"
          value={formData.firstName}
          onChange={handleInputChange}
          className="mt-1 block w-full p-2 border border-gray-300 rounded-md"
        />
        {formErrors.firstName && (
          <p className="text-red-500 text-sm mt-1">{formErrors.firstName}</p>
        )}
      </div>

      <div className="mb-4">
        <label htmlFor="lastName" className="block text-sm font-medium text-gray-700">
          Last Name
        </label>
        <input
          type="text"
          id="lastName"
          name="lastName"
          value={formData.lastName}
          onChange={handleInputChange}
          className="mt-1 block w-full p-2 border border-gray-300 rounded-md"
        />
        {formErrors.lastName && (
          <p className="text-red-500 text-sm mt-1">{formErrors.lastName}</p>
        )}
      </div>

      <div className="mb-4">
        <label htmlFor="email" className="block text-sm font-medium text-gray-700">
          Email
        </label>
        <input
          type="email"
          id="email"
          name="email"
          value={formData.email}
          onChange={handleInputChange}
          className="mt-1 block w-full p-2 border border-gray-300 rounded-md"
        />
        {formErrors.email && (
          <p className="text-red-500 text-sm mt-1">{formErrors.email}</p>
        )}
      </div>

      <button
        type="submit"
        className="w-full bg-blue-500 text-white py-2 px-4 rounded-md"
      >
        Submit
      </button>
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

Explanation
Let's break down what's happening in the code:

  1. Defining the Schema with Zod We start by defining a schema using Zod. This schema enforces the rules for each form field. For instance, firstName and lastName are required fields, and email must be a valid email address.

typescript

const formSchema = z.object({
  firstName: z.string().min(1, "First Name is required"),
  lastName: z.string().min(1, "Last Name is required"),
  email: z.string().email("Invalid email address"),
});
Enter fullscreen mode Exit fullscreen mode
  1. Managing State We manage the form data using React's useState hook. This allows us to keep track of the user's input and any validation errors.

typescript

const [formData, setFormData] = useState<FormData>({
  firstName: '',
  lastName: '',
  email: '',
});
Enter fullscreen mode Exit fullscreen mode
const [formErrors, setFormErrors] = useState<Partial<FormData>>({});
Enter fullscreen mode Exit fullscreen mode
  1. Handling Input Changes The handleInputChange function updates the state as the user types in each input field.

typescript

const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
  const { name, value } = event.target;
  setFormData({ ...formData, [name]: value });
};
Enter fullscreen mode Exit fullscreen mode
  1. Handling Form Submission When the form is submitted, we validate the data using Zod's safeParse method. If the validation fails, we display error messages next to the relevant fields. If the validation succeeds, we proceed with form submission (in this case, simply logging the data and displaying an alert).

typescript

const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
  event.preventDefault();

  const result = formSchema.safeParse(formData);

  if (!result.success) {
    const errors: Partial<FormData> = {};
    result.error.errors.forEach((error) => {
      if (error.path[0]) {
        errors[error.path[0] as keyof FormData] = error.message;
      }
    });
    setFormErrors(errors);
  } else {
    setFormErrors({});
    alert('Form submitted successfully!');
    console.log(result.data);
  }
};
Enter fullscreen mode Exit fullscreen mode

Conclusion
In this post, we created a fully functional and type-safe form using React, TypeScript, and Zod. This setup ensures that your form handles user input and validation in a robust manner. By leveraging Zod, you can easily extend this approach to more complex forms and validation rules.

Image of Timescale

🚀 pgai Vectorizer: SQLAlchemy and LiteLLM Make Vector Search Simple

We built pgai Vectorizer to simplify embedding management for AI applications—without needing a separate database or complex infrastructure. Since launch, developers have created over 3,000 vectorizers on Timescale Cloud, with many more self-hosted.

Read full post →

Top comments (2)

Collapse
 
noartem profile image
Noskov Artem •

This can be made easier by using libraries such as Conform

Collapse
 
ezzeddinp profile image
Mohammad Ezzeddin Pratama •

thats sounds new for me, Thanks man👍🏻

Image of Docusign

🛠️ Bring your solution into Docusign. Reach over 1.6M customers.

Docusign is now extensible. Overcome challenges with disconnected products and inaccessible data by bringing your solutions into Docusign and publishing to 1.6M customers in the App Center.

Learn more

đź‘‹ Kindness is contagious

Dive into an ocean of knowledge with this thought-provoking post, revered deeply within the supportive DEV Community. Developers of all levels are welcome to join and enhance our collective intelligence.

Saying a simple "thank you" can brighten someone's day. Share your gratitude in the comments below!

On DEV, sharing ideas eases our path and fortifies our community connections. Found this helpful? Sending a quick thanks to the author can be profoundly valued.

Okay