Guides
Last updated
January 19, 2026

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

Nicolas Rios
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:

E46f966b

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! 🚀

Frequently Asked Questions

What is type-safe form validation in Next.js 15 and why does it matter?

Type-safe form validation means your validation rules, TypeScript types, and runtime checks all come from a single source of truth (a Zod schema) rather than being duplicated across client and server code. In Next.js 15, this matters because forms span both client components and Server Actions, and without a shared schema your client-side checks can silently drift out of sync with your backend rules.

How do Zod and React Hook Form work together in Next.js 15?

You define a Zod schema that describes the shape and rules of your form data, then pass it to React Hook Form via zodResolver from the @hookform/resolvers package. React Hook Form uses that schema to validate field values on the client in real time, while the same Zod schema can also run in your Server Action to validate the incoming request on the server, with no logic duplication required.

When should I use a Next.js 15 Server Action for form validation instead of handling everything on the client?

Use a Server Action when the validation requires something the client cannot safely do, such as checking a database, calling a third-party API, or validating sensitive business rules. For example, checking whether an email address is deliverable via a service like Abstract's Email Validation should happen inside a Server Action, not in a client-side Zod refine callback that would expose your API key and fire on every keystroke.

Why shouldn't I put async email validation inside Zod's refine method on the client?

Calling an external API inside refine on the client is expensive and slow: it would fire on every validation trigger (including each keystroke if validation is set to onChange), exposing your API key in the browser and hammering the external service. The recommended pattern is to keep synchronous format checks in Zod's refine, and reserve async checks like SMTP or disposability lookups for the Server Action or an optional onBlur event handler.

What does Abstract's Email Validation check that a regex cannot?

A regex can only confirm that an email address is formatted correctly. Abstract's Email Validation goes further by checking whether the domain has valid MX records, whether the mailbox actually exists via an SMTP handshake, and whether the address belongs to a disposable or temporary email provider. These checks catch addresses that look valid but would bounce or belong to throwaway accounts, which a regex alone cannot detect.

How do I infer TypeScript types from a Zod schema in Next.js 15?

Use z.infer<typeof YourSchema> to derive a TypeScript type directly from the schema. You can then use that type in your React Hook Form's useForm<YourType> call, in your Server Action's parameter type, and anywhere else in the codebase that handles that data. This ensures your types never drift from your validation rules because they are generated from the same definition.

Nicolas Rios
Nicolas Rios

CEO 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