Skip to main content

Form validation

Before submitting a form, of course we'd like to check if the input is valid. Are all required fields filled out? Do we need a minimum or maximum amount of characters or are there other additional requirements (including numbers or special characters when setting up a password, for example)?

If the information is correctly formatted, the application allows the data to be submitted to the server and (usually) saved in a database; if the information isn't correctly formatted, it gives the user an error message explaining what needs to be corrected, and lets them try again.

There are different ways to validate data. We will focus on built-in validation and a server-side validation pattern that fits seamlessly with Next.js' Server Actions (which we will cover in the next chapter).

Built-in form validation

There are several HTML5 form validation features, which are very easy to use. We don't have to rely on JavaScript for this type of validation. There are various validation attributes that can be used on <input>, <select> or <text-area> elements, such as:

  • required
  • minlength and maxlength for strings
  • min and max for numerical input types
  • type, to specify a preset type, such as password or email

See MDN for the many different types and other attributes.

For styling an invalid input element, we can use the :invalid CSS-pseudo-class.

Server-side validation

To build a server-side validation schema, we use the Zod library. This library enables us to declare a schema to ensure that your form inputs have the correct type. On top of that you can specify a custom error message.

First, store your addCalendarClient function in a actions.ts file in the root of your app. Move the 'use server'.

In your actions.ts file, where you stored your Server Action declare the following schema:

'use server'

import { z } from 'zod'

const schema = z.object({
client: z.string()
type: z.string(),
startTimestamp: z.string().refine((date) => !isNaN(Date.parse(date)), {
message: 'Invalid date format',
}),
stopTimestamp: z.string().refine((date) => !isNaN(Date.parse(date)), {
message: 'Invalid date format',
}),
})

// Rest of the code...

This schema ensures that inputs are strings and that the startTimestamp and stopTimestamp also have a correct ISO string format. Please check the further reading section to learn more about the refine() method of Zod. We check if the ISO string can actually be parsed by the Date object. If the return value is falsy, we know that the ISO string format is invalid.

The schema can then be used within the Server Action to validate all the fields.

After you've done that you can validate your formData easily in within Server Action like this:

export const createCalendarEvent = async (
initialState: any,
formData: FormData,
) => {
const date = formData.get('date');
const from = formData.get('from');
const to = formData.get('to');

const validatedFields = schema.safeParse({
client: formData.get('client'),
type: formData.get('activity'),
startTimestamp: `${date}T${from}:00.000Z`,
stopTimestamp: `${date}T${to}:00.000Z`,
});

if (!validatedFields.success) {
return validatedFields.error?.flatten().fieldErrors;
}

console.log('validatedFields', validatedFields.data);
};

Then, in your form component, a slight refactor is necessary to be able to show potential error messages. We make sure that the <Form /> becomes a client component, and then we import useActionState. This hook allows us to show to access the state of the form, plus we have access to the pending boolean. The form then looks something like this:

note

Note that when your Server Action is processed through the useActionState, the first argument of the action is the initialState and the second argument the formData.

'use client'

import { useActionState } from 'react'
import { createCalendarEvent } from '@/app/actions'

const initialState = {
message: '',
}

export function Form() {
const [state, formAction, pending] = useActionState(createCalendarEvent, initialState)

return (
<form action={formAction}>
<label htmlFor="email">Client</label>
<input id="client" name="client" required type="text" />
{/* ... */}
<p aria-live="polite">{state?.message}</p>
<button disabled={pending}>Add event</button>
</form>
)
}

This way the validation messages can be shown easily, and we can disable the button conditionally with the pending prop.

Further reading