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.
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!
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 create-next-app@latest 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.
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( [true], '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:
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 { register, handleSubmit, setValue, 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.
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.
<form action="#" method="POST" onSubmit={handleSubmit(onSubmit)} > <input type="hidden" name="remember" defaultValue="true" /> {/* name field */} <div> <label htmlFor="fullName" > Full name </label> <input type="text" {...register('fullName')} id="fullName" aria-describedby="nameHelp" /> {errors.fullName && ( <small id="emailHelp" > Name is a required field </small> )} </div> {/* company email field */} <div className="form-group mb-4"> <label htmlFor="companyEmail" > Company email </label> <input type="email" {...register('companyEmail')} id="companyEmail" aria-describedby="emailHelp" /> {errors.companyEmail && ( <small id="emailHelp" > Email is a required field </small> )} </div> {/* phone number field */} <div className="form-group mb-4"> <label htmlFor="phoneNumber" > Phone number </label> <input type="number" {...register('phoneNumber')} id="phoneNumber" aria-describedby="numberHelp" /> {errors.phoneNumber && ( <small id="emailHelp" > Phone number is required field </small> )} </div> {/* company website optional field */} <div> <label htmlFor="companyWebsite" > Website </label> <input type="text" {...register('companyWebsite')} id="companyWebsite" aria-describedby="websiteHelp" /> {errors.companyWebsite && ( <small id="websiteHelp" > Your website is incorrect </small> )} </div> {/* company size field */} <div className="form-group"> <label htmlFor="companySize" > Company size </label> <select aria-label="Select an option" {...register('companySize')} onChange={(e) => setValue('companySize', e.target.value, { 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> </select> {errors.companySize && ( <small id="sizeHelp" > Select company size </small> )} </div> {/* checkbox field */} <div> <div > <input id="remember-me" type="checkbox" {...register('acceptTerms')} /> <label htmlFor="remember-me" > I hereby confirm all the information provided is true and accurate. </label> </div> {errors.acceptTerms && ( <small > Accept our terms and conditions </small> )} </div> <button type="submit" > Get in touch </button> </form>
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( 'http://localhost:8888/.netlify/functions/formSubmit', { method: 'POST', body: JSON.stringify({ query: data, }), } ); console.log(response, 'Form submitted successfully'); } catch (err) { console.log(err); } finally{ 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.
[build] functions = 'functions' public = 'build'
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.
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!
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!
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 — start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowThe use cases for the ResizeObserver API may not be immediately obvious, so let’s take a look at a few practical examples.
Automate code comments using VS Code, Ollama, and Node.js.
Learn to build scalable micro-frontend applications using React, discussing their advantages over monolithic frontend applications.
Build a fully functional, real-time chat application using Laravel Reverb’s backend and Vue’s reactive frontend.