Menu

14. Forms & Validation

Next.js Master Roadmap - IT Technology

This chapter shows how to create robust, type‑safe forms in a Next.js application. You’ll learn to register inputs with React Hook Form, apply built‑in and custom validation, leverage Zod schemas for runtime validation and TypeScript inference, and handle submissions via Next.js Server Actions that interact directly with a database.

No MCQ questions available for this chapter.

14. Forms & Validation

14. Forms & Validation

Forms are a cornerstone of web applications, yet they are often a source of bugs, duplicated validation logic, and poor type safety. This chapter walks through a modern stack that eliminates those pain points:

  • React Hook Form (RHF) – minimal re‑renders, easy registration, and built‑in error handling.
  • Zod – declarative schemas that validate data at runtime and infer exact TypeScript types.
  • Next.js Server Actions – server‑side form processing without writing an API route, enabling direct database calls.

By the end of this chapter you will be able to build a form that:

  1. Registers each input with register and attaches validation rules.
  2. Uses a Zod schema as a resolver so validation errors are automatically synced with RHF.
  3. Submits via a Server Action, validates the payload with Zod, persists data with an ORM, and returns typed success/error responses.
  4. Displays field‑level errors and provides a smooth user experience.

React Hook Form Basics

Setting Up the Form

Start by initializing RHF with useForm. The returned object gives you register, handleSubmit, and formState (which contains errors and isSubmitting).

import { useForm } from "react-hook-form";

export default function Signup() {
  const { register, handleSubmit, formState: { errors } } = useForm();
  const onSubmit = (data) => console.log(data);
  return (
    
{/* inputs go here */}
); }

Registering Inputs

Each input receives the spreaded result of register(name, options). The name must match the field you want in the form state.

<input {...register("email")} />
<input {...register("password")} />

Built‑In Validation Rules

Pass an object as the second argument to register. RHF supports the following validators out of the box:

  • required – boolean or string message.
  • min / max – numeric value.
  • minLength / maxLength – string length.
  • pattern – RegExp with optional custom message.
  • validate – custom function returning boolean or string message.
<input
  {...register("email", {
    required: "Email is required",
    pattern: {
      value: /^\S+@\S+$/i,
      message: "Invalid email format",
    },
  })}>
<input
  {...register("password", {
    required: "Password is required",
    minLength: { value: 8, message: "At least 8 characters" },
  })}>

Errors are automatically placed in formState.errors. Render them conditionally:

{errors.email && <span className="error">{errors.email.message}</span>}
{errors.password && <span className="error">{errors.password.message}</span>}

Custom Validator Functions

When built‑in rules aren’t enough, use the validate property. It can return true, false, or a string message.

<input
  {...register("username", {
    validate: (value) =>
      /^[a-zA-Z0-9_]+$/.test(value) ||
      "Only letters, numbers, and underscores allowed",
  })}>

Handling Submission

handleSubmit wraps your submit handler and only calls it when the form passes validation. It receives the validated data.

const onSubmit = (data) => {
  // data is typed according to the resolver (see Zod section)
  console.log("Submitted:", data);
};

If you need to perform asynchronous work (e.g., an API call), simply make the handler async and await inside.

const onSubmit = async (data) => {
  await fetch("/api/users", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(data),
  });
};

Resetting the formState also exposes reset to clear fields or set default values.

const { reset } = useForm();
// Call reset() to clear, or reset({ email: "user@example.com" }) to pre‑fill.

Integrating Zod for Schema‑First Validation

Why Zod?

React Hook Form validates well, but keeping validation rules in two places (JSX and TypeScript) leads to drift. Zod lets you define a single source of truth:

  • Runtime validation (schema.parse / schema.safeParse).
  • Compile‑time TypeScript inference via z.infer<typeof schema>.

Defining a Zod Schema

import { z } from "zod";

const signupSchema = z.object({
  email: z.string().email({ message: "Invalid email" }),
  password: z.string()
    .min(8, { message: "Password must be at least 8 characters" })
    .regex(/[A-Z]/, { message: "Password needs an uppercase letter" }),
  // optional: confirm password via refinement
  confirmPassword: z.string(),
}).refine((data) => data.password