How to Implement an Account Registration Screen Using Server-Side Validation (Next.js & Zod)
Introduction
Up to the previous article, we used React Hook Form and Zod—libraries that provide the necessary features for input forms—to build an “input form” and implemented it so that when the “Create account” button is clicked, the input values can be checked in the console log.
This time, we’ll implement the process of passing the input values to a server action. Ultimately, we’ll register the data in a database, but since the process is a bit complex, the explanation will be split into two articles.
Goal of This Article
The goal of this article is to input a name, email address, and password on the account registration screen.
After that, we’ll verify the input values in a server action and, if an error occurs, implement a process that returns an error notification to the input screen.
Creating the Server Action
First, create a new actions folder and then create a register.ts file.
'use server'
import { z } from 'zod'
import { RegisterSchema } from '@/schema'
export const register = async (values: z.infer<typeof RegisterSchema>) => {
const validatedFields = RegisterSchema.safeParse(values)
console.log(validatedFields.data)
if (!validatedFields.success) {
return { error: 'Please correct your input' }
}
return { error: 'An error occurred' }
}
By defining 'use server', this function will be called on the server side.
We use Zod, which we introduced in the previous article, to validate the received data.
One of the convenient aspects of Zod is that it can be used in the same way on both the client side and the server side.
We validate the received values, and if an error occurs,
we can return an error message to the client that called the action.
In this case, since we're applying the same validation on the client side, in principle the message Please correct your input should almost never be returned.
So we can basically ignore this branch for now and return a message to the user in the final return.
For testing purposes, with the current settings this server action will return an error 100% of the time (we’ll fix this later).
Configuring the Client Side to Call the Server Action
Now let’s modify the account registration screen so that it calls the server action we just created.
+ import { register } from '@/actions/register'
(※some parts omitted)
- const onSubmit = (values: z.infer<typeof RegisterSchema>) => {
+ const onSubmit = async (values: z.infer<typeof RegisterSchema>) => {
+ const response = await register(values)
- console.log(values)
+ console.log(response)
}
Since the server action is set up as an asynchronous process, don’t forget to add async to onSubmit when calling the server action.
After calling the server action, we check the response in the console log.
When you fill out the account registration form and click the button, the response from the server action will be displayed on the client side,
and on the server side you should see the contents of the input form.
If you see output like this, you’re good to go.
We’ve successfully passed the contents of the input form to the server side.
Notifying the User of the Result of the Server Action
Right now, the result of the server action is only shown in the console log, so we’ll display it in the input form to notify the user whether account creation succeeded or failed.
We’ll implement this in three main steps:
- ① Create notification components
- ② Add the components to the input form
- ③ Update
onSubmitbased on the server action response to display the notification components
① Create Notification Components
We’ll create separate components for when the server action succeeds and when it fails.
import { CheckCircledIcon } from '@radix-ui/react-icons'
interface FormSuccessProps {
message?: string
}
export const FormSuccess = ({ message }: FormSuccessProps) => {
if (!message) return null
return (
<div className="flex items-center gap-x-2 rounded-md bg-emerald-500/15 p-3 text-sm text-emerald-500">
<CheckCircledIcon className="size-4" />
<p>{message}</p>
</div>
)
}
import { ExclamationTriangleIcon } from '@radix-ui/react-icons'
interface FormErrorProps {
message?: string
}
export const FormError = ({ message }: FormErrorProps) => {
if (!message) return null
return (
<div className="flex items-center gap-x-2 rounded-md bg-destructive/15 p-3 text-sm text-destructive">
<ExclamationTriangleIcon className="size-4" />
<p>{message}</p>
</div>
)
}
Both components behave basically the same: if they receive a message, they return a component; if there’s no message, they render nothing.
To make things clearer for users, we’re also using icons. @radix-ui/react-icons is the icon set that gets installed together with shadcn when you first set it up.
② Add the Components to the Input Form
Next, we’ll add the components we created to the input form.
We’ll manage the messages included in the server action response using useState.
+ import { useState } from 'react'
+ import { FormError } from '../form-error'
+ import { FormSuccess } from '../form-success'
(※some parts omitted)
export const RegisterForm = () => {
+ const [error, setError] = useState<string | undefined>("")
+ const [success, setSuccess] = useState<string | undefined>("")
const form = useForm<z.infer<typeof RegisterSchema>>({
resolver: zodResolver(RegisterSchema),
defaultValues: {
email: '',
password: '',
name: '',
},
})
(※some parts omitted)
/>
</div>
+ <FormError message={error} />
+ <FormSuccess message={success} />
<Button type="submit" className="w-full">
Create Account
</Button>
</form>
③ Update onSubmit Based on the Server Action Response to Display the Notification Components
Finally, we’ll update onSubmit so that when an error is returned, the error component is displayed in the input form.
- import { useState } from 'react'
+ import { useState, useTransition } from 'react'
const onSubmit = async (values: z.infer<typeof RegisterSchema>) => {
- const response = await register(values)
- console.log(response)
+ setError('')
+ setSuccess('')
+ setTransition(async () => {
+ try {
+ const response = await register(values)
+ if (response.error) {
+ setError(response.error)
+ } else {
+ // setSuccess(response.success)
+ }
+ } catch (e) {
+ setError('An error occurred')
+ }
+ })
}
(※some parts omitted)
- <Button type="submit" className="w-full">
+ <Button type="submit" className="w-full" disabled={isPending}>
Create Account
</Button>
At the very beginning of onSubmit, we reset the messages. Since the previous message content is still there, we clear it and then store new messages based on the result of the server action.
Using React’s useTransition, we disable the “Create account” button until the server action returns a result. This prevents users from accidentally clicking the button twice.
We didn’t implement it this time, but it’s also a good idea to set disabled={isPending} on the Input components as well.
Operation Check
Once again, if you fill out the account registration form and click the button, you should see an error message displayed.
We’ve now successfully called the server action and notified the user of the result.
Conclusion
In this article, we implemented the process of passing input form data to a server action.
We also implemented a mechanism that assumes the process fails, returns the result to the input form, and notifies the user.
In the next article, we’ll implement functionality to register the received data in a database via the server action. At the same time, we’ll update the current server action—which always returns an error—so that it switches the message content based on the result of the database registration process.
Next Article
Questions about this article 📝
If you have any questions or feedback about the content, please feel free to contact us.Go to inquiry form
Related Articles
Chat App (with Image/PDF Sending and Video Call Features)
2024/07/15Practical Component Design Guide with React × Tailwind CSS × Emotion: The Optimal Approach to Design Systems, State Management, and Reusability
2024/11/22Management Dashboard Features (Graph Display, Data Import)
2024/06/02Tutorial for Implementing Authentication with Next.js and Auth.js
2024/09/13Thorough Comparison of the Best ORMs for the Next.js App Router: How to Choose Between Prisma / Drizzle / Kysely / TypeORM [Part 1]
2025/03/13Test Strategy in the Next.js App Router Era: Development Confidence Backed by Jest, RTL, and Playwright
2025/04/22Done in 10 minutes. Easy deployment procedure for a Next.js app using the official AWS Amplify template
2024/11/05Building an Integrated Next.js × AWS CDK Environment: From Local Development with Docker to Production Deployment
2024/05/11


