Next.js & Form Validation: A Beginner's Guide to User Input

Forms are the backbone of almost every interactive website. They’re how users provide information, sign up for newsletters, submit feedback, and much more. In the world of web development, especially with a framework like Next.js, handling forms efficiently and effectively is crucial. This tutorial will guide you through the process of building and validating forms in Next.js, ensuring a smooth and reliable user experience. We’ll cover everything from basic setup to advanced validation techniques, helping you create robust and user-friendly forms.

Why Form Validation Matters

Imagine a scenario: a user is trying to sign up for your service, but they accidentally mistype their email address. Without proper validation, the form might submit with the incorrect information, leading to failed communications and a frustrated user. Form validation prevents these issues by:

  • Ensuring Data Integrity: Validating ensures that the data submitted is in the correct format and meets specific requirements.
  • Improving User Experience: Providing immediate feedback to users helps them correct errors and understand what’s expected.
  • Preventing Security Issues: Properly validated forms can help protect against malicious attacks by filtering out harmful inputs.
  • Reducing Server Load: Validating on the client-side can reduce the number of invalid requests sent to the server.

In essence, form validation is about building trust with your users and ensuring the reliability of your application.

Setting Up Your Next.js Project

If you don’t already have a Next.js project, let’s create one. Open your terminal and run the following command:

npx create-next-app my-form-app
cd my-form-app

This will create a new Next.js project named “my-form-app” and navigate you into the project directory. Next, let’s install a form library to make form handling easier. We’ll use Formik, a popular library for building forms in React (and thus, Next.js):

npm install formik

Formik simplifies form state management, validation, and submission, making it an excellent choice for this tutorial.

Creating a Basic Form

Let’s create a simple form within a Next.js page. Open the `pages/index.js` file (or your preferred page file) and replace its content with the following code:

import { useFormik } from 'formik';

function MyForm() {
  const formik = useFormik({
    initialValues: {
      firstName: '',
      lastName: '',
      email: '',
    },
    onSubmit: values => {
      alert(JSON.stringify(values, null, 2));
    },
  });

  return (
    <form onSubmit={formik.handleSubmit}>
      <div>
        <label htmlFor="firstName">First Name</label>
        <input
          type="text"
          id="firstName"
          name="firstName"
          onChange={formik.handleChange}
          value={formik.values.firstName}
        />
      </div>

      <div>
        <label htmlFor="lastName">Last Name</label>
        <input
          type="text"
          id="lastName"
          name="lastName"
          onChange={formik.handleChange}
          value={formik.values.lastName}
        />
      </div>

      <div>
        <label htmlFor="email">Email Address</label>
        <input
          type="email"
          id="email"
          name="email"
          onChange={formik.handleChange}
          value={formik.values.email}
        />
      </div>

      <button type="submit">Submit</button>
    </form>
  );
}

export default MyForm;

Let’s break down this code:

  • Import `useFormik`: We import the `useFormik` hook from Formik to manage our form state and handle form submissions.
  • `initialValues`: This object sets the initial values for each form field.
  • `onSubmit`: This function is called when the form is submitted. In this example, it simply alerts the form values.
  • Form Fields: We have input fields for `firstName`, `lastName`, and `email`. The `onChange` event handler updates the formik state, and the `value` prop binds the input to the formik state.
  • `handleSubmit`: This function from Formik handles the form submission and triggers the `onSubmit` function.

Now, run your Next.js development server with `npm run dev` and navigate to your page (usually `http://localhost:3000`). You should see a basic form with input fields. When you submit the form, an alert will display the data entered.

Adding Validation with Yup

While Formik handles the form’s state and submission, we’ll use Yup, a JavaScript schema builder for value parsing and validation, to define our validation rules. Install Yup by running:

npm install yup

Next, let’s integrate Yup into our form. Modify your `pages/index.js` file as follows:


import { useFormik } from 'formik';
import * as Yup from 'yup';

function MyForm() {
  const formik = useFormik({
    initialValues: {
      firstName: '',
      lastName: '',
      email: '',
    },
    validationSchema: Yup.object({
      firstName: Yup.string().required('Required'),
      lastName: Yup.string().required('Required'),
      email: Yup.string().email('Invalid email address').required('Required'),
    }),
    onSubmit: (values, { setSubmitting }) => {
      setTimeout(() => {
        alert(JSON.stringify(values, null, 2));
        setSubmitting(false);
      }, 400);
    },
  });

  return (
    <form onSubmit={formik.handleSubmit}>
      <div>
        <label htmlFor="firstName">First Name</label>
        <input
          type="text"
          id="firstName"
          name="firstName"
          onChange={formik.handleChange}
          onBlur={formik.handleBlur}
          value={formik.values.firstName}
        />
        {formik.touched.firstName && formik.errors.firstName ? (
          <div className="error">{formik.errors.firstName}</div>
        ) : null}
      </div>

      <div>
        <label htmlFor="lastName">Last Name</label>
        <input
          type="text"
          id="lastName"
          name="lastName"
          onChange={formik.handleChange}
          onBlur={formik.handleBlur}
          value={formik.values.lastName}
        />
        {formik.touched.lastName && formik.errors.lastName ? (
          <div className="error">{formik.errors.lastName}</div>
        ) : null}
      </div>

      <div>
        <label htmlFor="email">Email Address</label>
        <input
          type="email"
          id="email"
          name="email"
          onChange={formik.handleChange}
          onBlur={formik.handleBlur}
          value={formik.values.email}
        />
        {formik.touched.email && formik.errors.email ? (
          <div className="error">{formik.errors.email}</div>
        ) : null}
      </div>

      <button type="submit" disabled={formik.isSubmitting}>
        {formik.isSubmitting ? 'Submitting...' : 'Submit'}
      </button>
    </form>
  );
}

export default MyForm;

Here’s what’s changed:

  • Import Yup: We import the Yup library.
  • `validationSchema`: We add a `validationSchema` property to the `useFormik` configuration. This property is where we define our validation rules using Yup.
  • Yup Schema: Inside `Yup.object()`, we define the validation rules for each field. For example:
    • `firstName: Yup.string().required(‘Required’)`: This means the `firstName` field must be a string and is required. If it’s not provided, it will display the error message ‘Required’.
    • `email: Yup.string().email(‘Invalid email address’).required(‘Required’)`: This means the `email` field must be a string, must be a valid email format, and is required.
  • `onBlur`: We add the `onBlur` event handler to each input field. This triggers validation when the user moves focus away from the input field.
  • Error Display: We conditionally render error messages below each input field using `formik.touched` and `formik.errors`. `formik.touched` indicates whether the field has been blurred (touched), and `formik.errors` contains the validation errors.
  • Submitting State: We’ve added `formik.isSubmitting` to disable the submit button while the form is submitting, and to display a “Submitting…” message.

Now, when you try to submit the form with invalid data, the error messages defined in the Yup schema will appear below the corresponding fields. This provides immediate feedback to the user, allowing them to correct their input before submission.

Custom Validation Rules

While Yup provides many built-in validation methods, you can also define custom validation rules. This is useful for more complex validation scenarios. For example, let’s add a custom validation rule to ensure the first name doesn’t contain any numbers. Update your `pages/index.js` file:


import { useFormik } from 'formik';
import * as Yup from 'yup';

function MyForm() {
  const formik = useFormik({
    initialValues: {
      firstName: '',
      lastName: '',
      email: '',
    },
    validationSchema: Yup.object({
      firstName: Yup.string()
        .required('Required')
        .matches(/^[a-zA-Zs]+$/, 'Only alphabetic characters are allowed'),
      lastName: Yup.string().required('Required'),
      email: Yup.string().email('Invalid email address').required('Required'),
    }),
    onSubmit: (values, { setSubmitting }) => {
      setTimeout(() => {
        alert(JSON.stringify(values, null, 2));
        setSubmitting(false);
      }, 400);
    },
  });

  return (
    <form onSubmit={formik.handleSubmit}>
      <div>
        <label htmlFor="firstName">First Name</label>
        <input
          type="text"
          id="firstName"
          name="firstName"
          onChange={formik.handleChange}
          onBlur={formik.handleBlur}
          value={formik.values.firstName}
        />
        {formik.touched.firstName && formik.errors.firstName ? (
          <div className="error">{formik.errors.firstName}</div>
        ) : null}
      </div>

      <div>
        <label htmlFor="lastName">Last Name</label>
        <input
          type="text"
          id="lastName"
          name="lastName"
          onChange={formik.handleChange}
          onBlur={formik.handleBlur}
          value={formik.values.lastName}
        />
        {formik.touched.lastName && formik.errors.lastName ? (
          <div className="error">{formik.errors.lastName}</div>
        ) : null}
      </div>

      <div>
        <label htmlFor="email">Email Address</label>
        <input
          type="email"
          id="email"
          name="email"
          onChange={formik.handleChange}
          onBlur={formik.handleBlur}
          value={formik.values.email}
        />
        {formik.touched.email && formik.errors.email ? (
          <div className="error">{formik.errors.email}</div>
        ) : null}
      </div>

      <button type="submit" disabled={formik.isSubmitting}>
        {formik.isSubmitting ? 'Submitting...' : 'Submit'}
      </button>
    </form>
  );
}

export default MyForm;

The key change is the addition of the `.matches()` method to the `firstName` validation schema:

.matches(/^[a-zA-Zs]+$/, 'Only alphabetic characters are allowed')

This method uses a regular expression to validate the input. The regular expression `^[a-zA-Zs]+$` ensures that the input contains only letters (a-z, A-Z) and spaces. If the input doesn’t match this pattern, the error message ‘Only alphabetic characters are allowed’ is displayed. This is a powerful way to implement custom validation logic.

Handling Form Submission

In the previous examples, we used `alert()` to display the form data upon submission. In a real-world application, you would typically send this data to a server. Here’s how to handle form submission in Next.js, including sending data to an API route:

First, create an API route. In your Next.js project, create a file at `pages/api/submit.js`. This file will handle the form submission data. Here’s an example:


export default async function handler(req, res) {
  if (req.method === 'POST') {
    // Process a POST request
    try {
      const { firstName, lastName, email } = req.body;

      // Here you would typically save the data to a database or send it to another service.
      // For this example, we'll just log it to the console.
      console.log('Received form data:', { firstName, lastName, email });

      // Send a success response
      res.status(200).json({ message: 'Form submitted successfully!' });
    } catch (error) {
      console.error('Error processing form data:', error);
      res.status(500).json({ error: 'Failed to submit form' });
    }
  } else {
    // Handle any other HTTP method
    res.status(405).json({ error: 'Method Not Allowed' });
  }
}

In this API route:

  • We check if the request method is ‘POST’.
  • We extract the form data from `req.body`.
  • We simulate saving the data (in a real application, you’d save it to a database or send it to an external service).
  • We send a success response with a 200 status code.
  • We handle potential errors and send an appropriate error response.

Now, modify the `onSubmit` function in your `pages/index.js` file to send a POST request to this API route:


import { useFormik } from 'formik';
import * as Yup from 'yup';

function MyForm() {
  const formik = useFormik({
    initialValues: {
      firstName: '',
      lastName: '',
      email: '',
    },
    validationSchema: Yup.object({
      firstName: Yup.string()
        .required('Required')
        .matches(/^[a-zA-Zs]+$/, 'Only alphabetic characters are allowed'),
      lastName: Yup.string().required('Required'),
      email: Yup.string().email('Invalid email address').required('Required'),
    }),
    onSubmit: async (values, { setSubmitting, resetForm }) => {
      try {
        const response = await fetch('/api/submit', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
          },
          body: JSON.stringify(values),
        });

        if (response.ok) {
          // Form submission successful
          alert('Form submitted successfully!');
          resetForm(); // Reset the form after successful submission
        } else {
          // Form submission failed
          alert('Form submission failed.');
        }
      } catch (error) {
        console.error('Form submission error:', error);
        alert('An error occurred during form submission.');
      } finally {
        setSubmitting(false);
      }
    },
  });

  return (
    <form onSubmit={formik.handleSubmit}>
      <div>
        <label htmlFor="firstName">First Name</label>
        <input
          type="text"
          id="firstName"
          name="firstName"
          onChange={formik.handleChange}
          onBlur={formik.handleBlur}
          value={formik.values.firstName}
        />
        {formik.touched.firstName && formik.errors.firstName ? (
          <div className="error">{formik.errors.firstName}</div>
        ) : null}
      </div>

      <div>
        <label htmlFor="lastName">Last Name</label>
        <input
          type="text"
          id="lastName"
          name="lastName"
          onChange={formik.handleChange}
          onBlur={formik.handleBlur}
          value={formik.values.lastName}
        />
        {formik.touched.lastName && formik.errors.lastName ? (
          <div className="error">{formik.errors.lastName}</div>
        ) : null}
      </div>

      <div>
        <label htmlFor="email">Email Address</label>
        <input
          type="email"
          id="email"
          name="email"
          onChange={formik.handleChange}
          onBlur={formik.handleBlur}
          value={formik.values.email}
        />
        {formik.touched.email && formik.errors.email ? (
          <div className="error">{formik.errors.email}</div>
        ) : null}
      </div>

      <button type="submit" disabled={formik.isSubmitting}>
        {formik.isSubmitting ? 'Submitting...' : 'Submit'}
      </button>
    </form>
  );
}

export default MyForm;

In this updated `onSubmit` function:

  • We use the `fetch` API to send a POST request to `/api/submit`.
  • We set the `Content-Type` header to `application/json` to indicate that we’re sending JSON data.
  • We use `JSON.stringify(values)` to convert the form data to a JSON string.
  • We check the response status. If the response is successful (`response.ok`), we display a success message and reset the form using `resetForm()`.
  • If the submission fails, we display an error message.
  • We handle potential errors using a `try…catch…finally` block to ensure that `setSubmitting(false)` is always called.

Now, when you submit the form, the data will be sent to the API route, and you’ll see a success or error message based on the response. Check your browser’s developer console for the console logs from the API route.

Styling Your Forms

While the focus of this tutorial is on form validation, let’s briefly touch on styling. Next.js, being a React framework, allows you to use various styling approaches. Here are a few options:

  • CSS Modules: Create CSS files with the `.module.css` extension and import them into your components. This provides scoped styles, preventing style conflicts.
  • Styled Components: Use the `styled-components` library to write CSS-in-JS. This allows you to style your components directly within your JavaScript files.
  • Tailwind CSS: Tailwind CSS is a utility-first CSS framework that allows you to style your components by applying utility classes.

For a quick example, let’s add some basic styling using CSS Modules. Create a file named `MyForm.module.css` in the same directory as your `pages/index.js` file and add the following styles:


.form {
  display: flex;
  flex-direction: column;
  width: 300px;
  margin: 20px auto;
}

.input {
  margin-bottom: 10px;
  padding: 8px;
  border: 1px solid #ccc;
  border-radius: 4px;
}

.error {
  color: red;
  font-size: 0.8em;
  margin-top: 2px;
}

.button {
  padding: 10px 15px;
  background-color: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.button:disabled {
  background-color: #cccccc;
  cursor: not-allowed;
}

Then, import and apply these styles in your `pages/index.js` file:


import { useFormik } from 'formik';
import * as Yup from 'yup';
import styles from './MyForm.module.css'; // Import the CSS module

function MyForm() {
  const formik = useFormik({
    initialValues: {
      firstName: '',
      lastName: '',
      email: '',
    },
    validationSchema: Yup.object({
      firstName: Yup.string()
        .required('Required')
        .matches(/^[a-zA-Zs]+$/, 'Only alphabetic characters are allowed'),
      lastName: Yup.string().required('Required'),
      email: Yup.string().email('Invalid email address').required('Required'),
    }),
    onSubmit: async (values, { setSubmitting, resetForm }) => {
      try {
        const response = await fetch('/api/submit', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
          },
          body: JSON.stringify(values),
        });

        if (response.ok) {
          // Form submission successful
          alert('Form submitted successfully!');
          resetForm(); // Reset the form after successful submission
        } else {
          // Form submission failed
          alert('Form submission failed.');
        }
      } catch (error) {
        console.error('Form submission error:', error);
        alert('An error occurred during form submission.');
      } finally {
        setSubmitting(false);
      }
    },
  });

  return (
    <form onSubmit={formik.handleSubmit} className={styles.form}> {/* Apply the form class */} 
      <div>
        <label htmlFor="firstName">First Name</label>
        <input
          type="text"
          id="firstName"
          name="firstName"
          onChange={formik.handleChange}
          onBlur={formik.handleBlur}
          value={formik.values.firstName}
          className={styles.input}  // Apply the input class
        />
        {formik.touched.firstName && formik.errors.firstName ? (
          <div className={styles.error}>{formik.errors.firstName}</div> // Apply the error class
        ) : null}
      </div>

      <div>
        <label htmlFor="lastName">Last Name</label>
        <input
          type="text"
          id="lastName"
          name="lastName"
          onChange={formik.handleChange}
          onBlur={formik.handleBlur}
          value={formik.values.lastName}
          className={styles.input}
        />
        {formik.touched.lastName && formik.errors.lastName ? (
          <div className={styles.error}>{formik.errors.lastName}</div>
        ) : null}
      </div>

      <div>
        <label htmlFor="email">Email Address</label>
        <input
          type="email"
          id="email"
          name="email"
          onChange={formik.handleChange}
          onBlur={formik.handleBlur}
          value={formik.values.email}
          className={styles.input}
        />
        {formik.touched.email && formik.errors.email ? (
          <div className={styles.error}>{formik.errors.email}</div>
        ) : null}
      </div>

      <button type="submit" disabled={formik.isSubmitting} className={styles.button}> {/* Apply the button class */} 
        {formik.isSubmitting ? 'Submitting...' : 'Submit'}
      </button>
    </form>
  );
}

export default MyForm;

By importing the CSS module and applying the classes to the form elements, you can easily style your form. This approach keeps your styles scoped to the component, preventing potential conflicts with other styles in your application.

Common Mistakes and How to Fix Them

When working with forms and validation, developers often encounter similar issues. Here are some common mistakes and how to address them:

  • Incorrect Field Names: Make sure that the `name` attributes of your input fields match the keys in your `initialValues` object and the validation schema. Mismatched names can lead to data not being submitted or validated correctly.
  • Forgetting `onBlur`: Without the `onBlur` event handler, validation errors won’t be triggered when the user leaves a field. The `onBlur` event is crucial for providing real-time feedback.
  • Incorrect Error Display: Double-check that you’re correctly using `formik.touched` and `formik.errors` to display the error messages. Make sure the field names in the error display match the field names in your form.
  • Missing Validation Rules: Ensure that your validation schema covers all required fields and any specific formatting requirements (e.g., email, phone number). Omission of validation rules can lead to invalid data being submitted.
  • Not Handling Submission Errors: Always include error handling in your `onSubmit` function. This includes catching errors from the API calls and displaying appropriate error messages to the user.
  • Incorrect API Endpoint: Verify that the API endpoint URL in your `fetch` call is correct and that the API route is set up to handle the data correctly.
  • Not Resetting the Form: After a successful submission, consider resetting the form fields using `resetForm()` to provide a better user experience.

Key Takeaways

  • Formik simplifies form management: Use Formik to handle form state, submission, and validation.
  • Yup for validation: Leverage Yup to define and enforce validation rules.
  • Real-time feedback: Provide immediate feedback to users using error messages.
  • API Integration: Send form data to an API route for processing.
  • Styling options: Choose the styling approach that best suits your project.

FAQ

Here are some frequently asked questions about form validation in Next.js:

  1. Can I use other validation libraries besides Yup?

    Yes, you can. While Yup is popular and easy to integrate with Formik, you can also use other validation libraries like Zod or custom validation functions. The key is to integrate the validation logic with your form management library (like Formik).

  2. How do I handle complex validation scenarios?

    For more complex validation scenarios, you can use custom validation functions within Yup or create more sophisticated Yup schemas. You can also combine multiple validation rules and use conditional validation based on the values of other fields.

  3. How do I validate file uploads?

    File upload validation typically involves checking the file type, size, and other properties before submitting the form. You can use Yup’s built-in validation methods or create custom validation functions to handle file uploads. You’ll also need to handle the file upload on the server-side, which is beyond the scope of this tutorial.

  4. How do I handle form validation on the server-side?

    Server-side validation is crucial for security and data integrity. You should always validate the form data on the server-side, even if you have client-side validation. You can use the same validation library (e.g., Yup) on the server-side or use a different validation approach. Server-side validation helps protect against malicious attacks and ensures that the data is valid before being processed.

Form validation is an essential aspect of building user-friendly and reliable web applications. By mastering the techniques described in this tutorial, you’ll be well-equipped to create robust forms in your Next.js projects. Remember to always prioritize user experience by providing clear feedback and ensuring that your forms are easy to use. The more you practice and experiment with different validation scenarios, the better you’ll become at handling forms effectively and creating a positive experience for your users. The careful implementation of validation, combined with well-structured code, will not only improve the quality of your applications but also contribute to a more secure and trustworthy online environment.