Ishan Manandhar Ishan is a passionate product designer and frontend developer. He likes learning and implementing new tech stacks. He frequently writes blogs and also runs his YouTube channel, For Those Who Code.

Form validation with Next.js and Netlify

6 min read 1798

Form Validation With Next.js And Netlify

Building forms is a vital crux for many software applications. Forms are generally meant for collecting data from users.

With the modern enhancement of JAMStack technologies and serverless approaches, most of the things that used to require a backend can now be handled completely on the frontend or through APIs. But sometimes this is not enough, and that’s where serverless functions kick in.

In this article, we will be building fully serverless forms. We’ll use Netlify Lambda functions, which allow us to run server-side code with almost no extra configuration. Netlify incorporates nicely with Next.js.

N.B., You can submit simple forms without the need for the Netlify function through simply Netlify forms as well.

What is a serverless function?

A serverless function is a chunk of code that can be executed on an on-demand basis.

Serverless functions will scale our application since they don’t require a 24/7 runtime. Essentially, we are using an on-demand approach by only utilizing the necessary computing processes.

We’ll demonstrate how we can bring together the power of building serverless forms using Netlify Lambda functions. Let’s jump into it!

Project setup

In this project, we’ll be using quite a few libraries that help us submit the required data to our server. We will start by creating a blank Next.js starter project with the command below:

npx [email protected] next-netlify-forms --typescript

We created a blank Next project with TypeScript called next-netlify-forms. We’ll add a couple of dependencies to start building our validated forms called to a serverless Netlify function.

Below is the code for the packages we will install:

npm i -D react-hook-form yup  @hookform/resolvers 
tailwindcss postcss autoprefixer 
npm i  @types/node ts-node  --save-dev

We are using React Hook Form for our client-side form validation, Yup as our schema validator, and we’ll spice up our styles with TailwindCSS for our UI.

Let’s start by defining our type for our data structure:

type formData = {
 fullName: string;
 companyEmail: string;
 phoneNumber: string;
 companyWebsite: string;
 companySize: string;
 acceptTerms: boolean;

We will have a couple of fields inside our forms that ultimately submit to our MongoDB database instance in MongoDB Atlas. MongoDB has a generous free tier to experiment with.

Validation with Yup

Now we’ll create a schema validator for our data using Yup. This will help us get rid of unwanted data saved inside our database. Yup comes in handy by expecting the type of data we need from users.

 const validateSchema = Yup.object().shape({
   fullName: Yup.string().required('Full name is required'),
   companyEmail: Yup.string()
     .email('Invalid email')
     .required('Email is required'),
   phoneNumber: Yup.string()
     .required('Phone number is required')
     .min(7, 'Phone must be at least 7 numbers')
     .max(12, 'UserPhonename must not exceed 12 characters'),
   companyWebsite: Yup.string().url('Invalid website URL'),
   companySize: Yup.string().required('Company size is required'),
   acceptTerms: Yup.boolean().oneOf(
     'You must accept the terms and conditions'

Great, we’ve defined our form validation! Let’s go over the fields we’re asking for and what they mean:

  • fullName: A required string
  • companyEmail: A required string that should be an email address
  • phoneNumber: A string that should be a minimum of seven characters and a maximum of 12
  • companyWebsite: An optional string that should be a URL
  • companySize: A required string that should be a number
  • acceptTerms: A required boolean

Now is a good time for us to add react-hook-form into the mix. It’ll provide us with a handy function to register, call an onSubmit handler, set values to our form fields, and keep track of error validation.

import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as Yup from 'yup';

 const {
   formState: { errors },
 } = useForm<formData>({
   mode: 'onChange',
   resolver: yupResolver(validateSchema),

The register() method allows us to register an element and apply the appropriate validation rules. The handleSubmit() function will receive the form data if validation is successful.

In this case, we are using formState to more easily return form errors, as well as setValues to keep a track of the values a user writes in the form fields.

Creating necessary form fields

Let’s create our markup with the respective form fields. We also need to track the data that users type into this form and call our serverless function later.

       <input type="hidden" name="remember" defaultValue="true" />
       {/* name field */}
           Full name
         {errors.fullName && (
             Name is a required field

       {/* company email field */}
       <div className="form-group mb-4">
           Company email
         {errors.companyEmail && (
             Email is a required field

       {/* phone number field */}
       <div className="form-group mb-4">
           Phone number
         {errors.phoneNumber && (
             Phone number is required field

       {/* company website optional field */}

         {errors.companyWebsite && (
             Your website is incorrect

       {/* company size field */}
       <div className="form-group">
           Company size
           aria-label="Select an option"
           onChange={(e) =>
             setValue('companySize',, {
               shouldValidate: true,
           <option value={''}>Select an option</option>
           <option value="0-9">Small, 0-9 employees</option>
           <option value="10-49">Medium, 10-49 employees</option>
           <option value="50+">Large, 50+ employees</option>
         {errors.companySize && (
             Select company size

       {/* checkbox field */}
         <div >
             I hereby confirm all the information provided is true and
         {errors.acceptTerms && (
             Accept our terms and conditions

           Get in touch

After we have made the skeleton for our form fields, we can now create an onSubmit handler that will invoke the serverless function we create. This function takes in data as a parameter, which is of object type formData that we previously defined. It calls an API with Fetch API and sends the form data as response body query data.

 const onSubmit = async (data: formData) => {
   try {
     const response = await fetch(
         method: 'POST',
         body: JSON.stringify({
           query: data,
     console.log(response, 'Form submitted successfully');
   } catch (err) {
     setValue('fullName', '');
     setValue('companyEmail', '');
     setValue('phoneNumber', '');
     setValue('companyWebsite', '');
     setValue('companySize', '');
     setValue('acceptTerms', false);

Notice that we are currently calling an endpoint at localhost with the URL http://localhost:8888/.netlify/functions/formSubmit that is not created yet. This is a serverless endpoint we will be creating inside of our applications.

To get into Netlify functions, we need to test this out locally. This is so we don’t need to deploy the application and test it in our Netlify servers.

npm i @netlify/functions

With this installed, we can get started with writing our serverless function that’ll later be executed in our Netlify server. By default, Netlify will look for your serverless function in the netlify/functions folder at the root of your project directory.

For this to happen, we need two files to be created. The first, formSubmit.ts, is inside a function directory. The other is a netlify.toml file for Netlify configuration. We will create the folders and file structure below.

├── functions
 ├── formSubmit.ts
├── src
├── public
├── ...
└── package.json
└── netlify.toml

The netlify.toml file contains the function congif needed for our serverless function to execute. It’s inside the functions folder and later can be accessed in the build folder.

 functions = 'functions'
 public = 'build'

Connecting to a database

Let’s write a function that’ll help us connect with our MongoDB database, netlify-forms. We can now import this inside of the Netlify function we create.

const MongoClient = require('mongodb').MongoClient;
const client = new MongoClient(process.env.MONGODB_URI, {
 useNewUrlParser: true,
 useUnifiedTopology: true,

async function connect() {
 if (!client.connect()) await client.connect();
 const db = client.db("netlify-forms");
 return { db, client };

export { connect };

Hang on tight, we’re almost there! We will now create our serverless function that’ll take in the event as a parameter. This is where we can take in queries and other data inside the object.

Creating a Netlify function

It’s time to create our Netlify function to handle the form data we pass in. These functions are hidden from public view, but they interact like no other API service!

More great articles from LogRocket:

These functions are synchronous with a maximum timeout of 10 seconds. If we need to add more time, we can change this ordinary function by adding a -background extension to its name.

import { Handler } from "@netlify/functions";
import { connect } from "../utils/database";

const handler: Handler = async (event: any) => {
 const { query } = JSON.parse(event.body);
 const { fullName, companyEmail, phoneNumber, companyWebsite, companySize } = query;
 const { db } = await connect();
 await db.collection("contact").insertOne({
   contacts: {
     Name: fullName,
     Email: companyEmail,
     Phone: phoneNumber,
     Website: companyWebsite,
     Size: companySize,
   createdAt: new Date(),
 return {
   statusCode: 200,
   body: JSON.stringify({ message: `ok` })

export { handler };

We destructured the parsed query and accessed all the data we passed to our function as a parameter event.

As the async function returns a promise, we end up returning a status code 200 with a call to our Mongo database, accessing it from the connect function we created. This is where we stored all our form data!

You can check out the Netlify documentation for a list of available event triggers.

With all of this in place, we can now send the required and validated data to call the serverless function! This will save the data to our database.


With freely accessible web APIs, we can leverage the powerful features this serverless function provides us with. We can achieve another level of possibilities with Netlify functions, indeed!

You can find the reference to the code repository in the link here.

Happy coding!

LogRocket: Full visibility into production Next.js apps

Debugging Next applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Next.js app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your Next.js apps — .

Ishan Manandhar Ishan is a passionate product designer and frontend developer. He likes learning and implementing new tech stacks. He frequently writes blogs and also runs his YouTube channel, For Those Who Code.

Leave a Reply