Implement Async Server-Side Validation

    Jack HerringtonJack Herrington

    Say that we're developing a form for a company that needs to register users, but they want to limit registrations to certain zip codes.

    We want to ensure that the zip code is valid and that we handle it appropriately, which might require using microservices or other complex processes.

    As a stand-in for a more complicated process, we'll say that we only handle zip codes that start with '9'.

    Building a Zip Code Validator

    To start, we'll create a new file at app/validateZipCode.ts for our server action.

    Inside of this file, we'll export an async validateZipCode function that accepts a zipcode string and returns a Promise with a Boolean value. Inside the function, we'll log the zipcode we're trying to check and return true if the zipcode is valid and starts with '9'. Since this is a server action, we'll add "use server" at the top of the file:

    "use server";
    export async function validateZipCode(zipCode: string): Promise<boolean> {
      console.log("validateZipcode on SERVER", zipCode)
      return /^\d{5}/.test(zipCode) && zipCode.startsWith('9');

    In a real-world application, you could perform all sorts of asynchronous processing within this function, including hitting various microservices, but we'll just stick with this.

    Update the Schema

    Over in src/app/registrationSchema.tsx, import validateZipcode:

    import { validateZipCode } from "./validateZipCode";

    Then, add a zipCode field to the schema and use the refine method to run the validateZipCode function. If there's an error, return the message "Invalid zipcode":

    export const schema = z.object({
    	// rest of the schema as before
    	zipCode: z.string().refine(validateZipCode, {
    		message: "Invalid zipcode",

    Update the Page

    Inside of the page.tsx file, we'll add a FormField for the zipcode:

    	render={({ field }) => (
    				<Input placeholder="" {...field} />
    			<FormDescription>Your zipcode (NNNNN).</FormDescription>
    			<FormMessage />

    Now when we attempt to submit the form, the console will display our validateZipcode on SERVER message with the number we put in. When we put in invalid numbers, we see the error message as expected.

    However, if we then input a valid zip code and hit submit, we encounter an internal server error.

    Fixing the Internal Server Error for Valid Zip Codes

    The problem arises from the fact that we've added the async validateZipCode to our registrationSchema, but the safeParse function in RegistrationForm.tsx is running synchronously. Trying to run async functions in synchronous mode won't work.

    Instead, we need to update the parsed variable to use the safeParseAsync function instead:

    export default function Home() {
    	const onFormAction = async (issues?: string[], formData: FormData) => {
    		"use server";
    		const data = Object.fromEntries(formData);
    		const parsed = await schema.safeParseAsync(data);

    Upon making these changes, the form accepts valid zip codes that begin with 9 and successfully registers users. The asynchronous validation of zip codes completes without a hitch!

    Now you know how to create custom server-side validations using Next.js server actions!


    So let's say that we're building this form for a company and that company wants to be able to register users, but we only handle certain zip codes of customers. So we want them to give us a zip code, we want to make sure that it's a valid zip code, but we want to make sure that we handle it. And in order to know if we handle it or not, actually, it's a fairly complicated process.

    We're going to go send that back to the server for processing. Maybe it goes and hits some microservices or something like that, who knows. But as a stand-in, we're just going to say that only zip codes starting with nine will actually validate. So let's go and first off build our validate zip code function.

    To do that, I'm going to create a new file in app called validate zip code dot ts. And into there, I'm going to bring a validate zip code function. It's going to take a zip code as a string and return a Boolean. Because it's an async function, it has to be an async function in order to be a server action.

    It has to return a promise to a Boolean. It's also going to do a console log to say that we're actually running on the server. Then it's going to check the format of the zip code to make sure that it's five numbers. And then it's going to validate whether that zip code starts with nine. Now you could do all kinds of async processing in here. You could hit microservices and all that sort of stuff.

    We're not going to do that. But how do we make sure that this will only run on the server? Well, we could put use server inside this function, or we just could put it at the top of the file. And now we can use validate zip code wherever we want, and it will just automatically, instead

    of running it locally on the client, it'll actually fetch back to the server, run it there, and return the response. Now we could do exactly the same thing over in the page. These two, onDataAction and onFormAction, could also be in their own file and has use server at the top and not be passed as props.

    It's really up to you. So we'll do validate zip code. So let's go and use validate zip code. So the first thing we want to do is connect it to our schema. So we're going to add a new zip code as a string.

    And now we need to bring in our custom validation. And in order to run it, we use the refine method. We simply give it the method, and then we give it the message back, which is invalid zip code. All right. Let's hit save.

    And now let's go back to our registration form, and we'll do zip code. We'll say your zip code. Looks good. We'll give it a default value. All right. Let's have a look. Hit submit.

    And we get an invalid zip code. Whoa. So what's actually happening? When we go back into our console, we can see that validate zip code is run on the server with that value. Let's try it again.

    Let's just give it five numbers. Again, invalid zip code. There we go. You can see it actually tracking. And let's add the nine. Hit submit. And now we're getting an internal server error. So what's actually happening?

    So what's happening is, well, we've added an async component to our registration schema. So this is now async, validate zip code. But we are on the page, and in this code, we are running this safe parse in a synchronous mode.

    So it's got asynchronous refinements, which we are trying to run synchronously, which isn't going to work. There is a safe parse async, and we should use that instead, and then we should await it. So if you've got async elements to your registration schema, in this case, or your schema, then you want to await those. All right.

    Let's give it a try. And there we go. User registered, and the zip code was checked asynchronously. Super cool.

    So now you know how to make server-side custom validations using Reactor form and Next.js server actions, and how to call that safe parse, but do it safely in an async way.