Guides
Last updated
January 19, 2026

Type-Safe Form Validation in Next.js 15 with Zod and React Hook Form

Nicolas Rios

Table of Contents:

Get your free
Email Validation
 API key now
stars rating
4.8 from 1,863 votes
See why the best developers build on Abstract
START FOR FREE
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.
No credit card required

Forms have always been one of the most fragile parts of a web application. They sit at the boundary between users and your backend, collecting untrusted input that can easily pollute your database if not handled carefully. For years, Next.js developers relied on a mix of useEffect, custom validation logic, and defensive backend checks to keep things under control. 😅

With Next.js 15, that era is officially over.

Thanks to Server Actions, combined with a mature ecosystem of form and schema libraries, we can now build forms that are type-safe, predictable, and deeply validated—without duplicating logic or fighting TypeScript.

In this guide, we’ll walk through the modern “holy trinity” of form handling in Next.js 15:

  • React Hook Form for fast, ergonomic client-side UI validation
  • Zod as a shared schema and type contract between client and server
  • AbstractAPI for real-world, asynchronous validation (like detecting disposable or non-existent email addresses)

By the end, you’ll have a copy-pasteable pattern for building production-grade forms that validate both syntax and reality. 🚀

Enter your email address to start
Need inspiration? Try
test@abstractapi.com
VALIDATE
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.
Checking
5
Results for
email
Deliverability:
TEST
Free email:
TEST
Valid format:
TEST
Disposable email:
TEST
Valid SMTP:
TEST
Valid MX record:
TEST
Get free credits, more data, and faster results

The New Standard for Forms in Next.js 15

Forms used to be hard—not because HTML forms are complicated, but because keeping validation consistent across the stack was. Client-side checks drifted from backend rules, TypeScript types went out of sync, and edge cases slipped through.

Next.js 15 changes this fundamentally.

Why Server Actions Matter

Server Actions allow you to run server-side logic directly from your React components—without creating API routes or manually handling fetch calls. That makes them ideal for form submissions, where validation and side effects naturally belong on the server.

But Server Actions alone aren’t enough.

We still need:

  • Immediate feedback in the UI
  • A single source of truth for validation rules
  • Asynchronous checks that go beyond regexes

That’s where the rest of the stack comes in.

The Stack at a Glance

Type-Safe Form Validation in Next.js 15 with Zod and React Hook Form

Together, they form a clean, scalable pattern that fits perfectly with Next.js 15’s architecture. ✨

Step 1: Project Setup and Dependencies

Let’s start with the essentials.

From your Next.js 15 project root, install the required dependencies:

npm install react-hook-form zod @hookform/resolvers

These libraries are fully compatible with the current Next.js ecosystem and work well alongside React’s latest features.

You’ll also want access to AbstractAPI’s Email Validation API, which you can learn more about on the AbstractAPI Email Validation page. 

This API will be used from our Server Action to validate emails asynchronously.

💡 Tip: Store your Abstract API key in an environment variable (ABSTRACT_API_KEY) and never expose it to the client.

Step 2: Defining the Schema (Your Type Contract)

A robust form starts with a schema. In this pattern, the schema is the contract between your UI and your backend.

Create a new file at: src/schemas/form.ts

Defining the Zod Schema

import { z } from "zod";

export const signupSchema = z.object({

  email: z.string().email("Please enter a valid email address"),

  password: z

    .string()

    .min(8, "Password must be at least 8 characters long"),

});

This schema does more than validation—it becomes the backbone of your type system.

Inferring Types from the Schema

export type SignupForm = z.infer<typeof signupSchema>;

This inferred type ensures that:

  • Your form fields
  • Your Server Action input
  • Your validation logic

are all guaranteed to stay in sync.

No duplicated interfaces. No manual typing. No drift. 🧠

Step 3: The Server Action (Where Logic Lives)

This is where Next.js 15 truly shines.

Server Actions let you keep sensitive logic—like API calls and validation—securely on the server, while still being easy to call from your components.

Create a new file: src/actions/signup.ts

And mark it as a Server Action: "use server";

Implementing the Validation Flow

Here’s the full logic we want:

  1. Parse and validate input using Zod
  2. If Zod passes, call AbstractAPI
  3. Translate API results into structured form errors

Example Server Action:

"use server";

import { signupSchema, SignupForm } from "@/schemas/form";

type ActionResult = {

  success: boolean;

  errors?: Partial<Record<keyof SignupForm, string>>;

};

export async function signupAction(

  data: SignupForm

): Promise<ActionResult> {

  const parsed = signupSchema.safeParse(data);

  if (!parsed.success) {

    const fieldErrors: ActionResult["errors"] = {};

    parsed.error.issues.forEach((issue) => {

      const fieldName = issue.path[0] as keyof SignupForm;

      fieldErrors[fieldName] = issue.message;

    });

    return { success: false, errors: fieldErrors };

  }

  const email = parsed.data.email;

  const response = await fetch( 

`https://emailvalidation.abstractapi.com/v1/?api_key=${process.env.ABSTRACT_API_KEY}&email=${email}`

  );

  const result = await response.json();

  if (result.is_disposable_email) {

    return {

      success: false,

      errors: {

        email: "Disposable email addresses are not allowed",

      },

    };

  }

  if (!result.is_valid_format || result.deliverability !== "DELIVERABLE") {

    return {

      success: false,

      errors: {

        email: "This email address does not appear to exist",

      },

    };

  }

  // At this point, the email is valid and trustworthy

  return { success: true };

}

Why This Layer Matters

Zod can tell you whether an email looks valid.

AbstractAPI tells you whether it’s actually usable.

This distinction is critical if you want to:

  • Prevent disposable signups
  • Reduce bounce rates
  • Keep your user database clean

You can learn more about these risks in AbstractAPI’s guides on preventing disposable emails.

Step 4: The Client Component (The UI)

Now let’s wire everything together in the UI.

Create a client component: "use client";

Setting Up React Hook Form with Zod

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

import { zodResolver } from "@hookform/resolvers/zod";

import { signupSchema, SignupForm } from "@/schemas/form";

import { signupAction } from "@/actions/signup";

export function SignupFormComponent() {

  const form = useForm<SignupForm>({

    resolver: zodResolver(signupSchema),

  });

  const {

    register,

    handleSubmit,

    setError,

    formState: { errors, isSubmitting },

  } = form;

  const onSubmit = async (data: SignupForm) => {

    const result = await signupAction(data);

    if (!result.success && result.errors) {

      Object.entries(result.errors).forEach(([field, message]) => {

        setError(field as keyof SignupForm, {

          message,

        });

      });

    }

  };

  return (

    <form onSubmit={handleSubmit(onSubmit)}>

      <input

        type="email"

        placeholder="Email"

        {...register("email")}

      />

      {errors.email && <p>{errors.email.message}</p>}

      <input

        type="password"

        placeholder="Password"

        {...register("password")}

      />

      {errors.password && <p>{errors.password.message}</p>}

      <button type="submit" disabled={isSubmitting}>

        Sign up

      </button>

    </form>

  );

}

UX Tip 🎯

Using form.setError() lets you surface server-side validation errors directly inside the form, exactly where users expect them—without alerts or generic messages.

This creates  seamless experience where client and server validation feel like a single system.

Advanced: Async Zod Refinement 

Zod supports asynchronous validation via refine, which makes it tempting to call APIs directly from the schema.

For example, you could check an email’s validity during validation:

z.string().email().refine(async (email) => {

  // async validation

});

But Be Careful ⚠️

Calling an external API:

  • On every keystroke
  • Or on every validation cycle

is expensive and slow—and can burn through API credits quickly.

Best Practice Recommendation

  • Use Zod for syntax and structure
  • Use Server Actions + AbstractAPI for deep validation
  • Optionally trigger async checks on blur, not on change

This keeps your app fast, scalable, and cost-efficient.

Handling Pending States in Next.js 15

Next.js 15 introduces useActionState (formerly useFormState) for handling Server Action state transitions.

While it’s powerful, for this pattern React Hook Form’s isSubmitting flag is usually simpler and clearer, especially when you’re already managing form state locally.

Use useActionState when:

  • You rely heavily on native <form action={...}>
  • You want framework-level pending handling

Otherwise, stick with what React Hook Form already gives you. 👍

Final Thoughts: A Future-Proof Form Stack

With this setup, you now have a form that:

Most importantly, you’ve eliminated an entire class of bugs caused by mismatched validation logic.

Ready to Secure Your Forms?

Don’t let fake users, disposable emails, or invalid data creep into your database.

👉 Get your free Abstract API key and start protecting your Next.js forms with real-world validation today:

https://www.abstractapi.com 

Happy coding! 🚀

Nicolas Rios

Head of Product at Abstract API

Get your free
Email Validation
key now
See why the best developers build on Abstract
get started for free

Related Articles

Get your free
Email Validation
key now
stars rating
4.8 from 1,863 votes
See why the best developers build on Abstract
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.
No credit card required