-
Notifications
You must be signed in to change notification settings - Fork 2
Post #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Post #1
Changes from all commits
914ef54
9e8905a
dd4f204
e12c977
13da384
333abf7
98833d1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,36 +1,13 @@ | ||
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). | ||
## Example app | ||
|
||
## Getting Started | ||
This example application uses Zod to validate a contact form. It demonstrates how to handle server-side validation errors and display them to the user. | ||
|
||
First, run the development server: | ||
See the [blog post](https://www.codingzeal.com/blog) for more information. | ||
|
||
## Running Locally | ||
|
||
```bash | ||
npm run dev | ||
# or | ||
yarn dev | ||
# or | ||
pnpm install | ||
pnpm dev | ||
# or | ||
bun dev | ||
open http://localhost:3000 | ||
``` | ||
|
||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. | ||
|
||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. | ||
|
||
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. | ||
|
||
## Learn More | ||
|
||
To learn more about Next.js, take a look at the following resources: | ||
|
||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. | ||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. | ||
|
||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! | ||
|
||
## Deploy on Vercel | ||
|
||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. | ||
|
||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,177 @@ | ||
# Using Zod for Form Validation with React Server Actions in Next.js | ||
|
||
Server-side validation is a critical line of defense against invalid and malicious data, and ensures data integrity and security. In this post, we will explore how to use [Zod](https://zod.dev/), a declarative JavaScript validation library, for server-side form validation in a [Next.js](https://nextjs.org/) application. We will also look into to handling validation errors returned from the server. | ||
|
||
## Sample Application | ||
|
||
This article assumes you're acquainted with setting up and running Next.js applications. | ||
|
||
To view the code referenced in this article, checkout the [repo](https://github.com/CodingZeal/nextjs-server-action-validation). | ||
|
||
This article and accompanying example application utilize Next.js version 14.x configured with TailwindCSS and TypeScript. | ||
|
||
## Overview of Zod Schema for Validation | ||
|
||
Zod allows us to define a validation schema for our form data. This schema is a declarative way of specifying the validation rules for each field. For example, to mark a field as required, we can use the `min(1)` method, which specifies that the field must have a minimum length of 1. The 2nd argument is optional and can be used to override the default error message. | ||
|
||
Zod has many validation methods for different data types and scenarios. You can use the `email()` method to validate an email address, or `url()` to validate a URL. Also, if you have custom needs, you can always use `regex()` to validate against a regular expression. | ||
|
||
Here's an example of a Zod schema definition: | ||
|
||
```ts | ||
import { z } from "zod"; | ||
|
||
const schema = z.object({ | ||
name: z.string().min(1, "Name cannot be blank"), | ||
email: z.string().email("Invalid email address"), | ||
}); | ||
``` | ||
|
||
house9 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
## Handling Server-Side Validation Errors | ||
|
||
When we submit our form, the server-side action validates the form data against the Zod schema. If the validation fails, the server returns an array of errors (`ZodIssue[]`). Each error object has the following structure: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd suggest a transition sentence here. Something like, "Using Zod, let's tease out a validation flow that integrates with Next.js. When we submit our form, a Next.js server-side action validates ...." There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Going to stick with my bland copy for this paragraph |
||
|
||
```json | ||
{ | ||
"code": "too_small", | ||
"minimum": 1, | ||
"type": "string", | ||
"inclusive": true, | ||
"exact": false, | ||
"message": "Name cannot be blank", | ||
"path": ["name"] | ||
} | ||
``` | ||
|
||
Now let's look at how we can implement this in our Next.js application. We'll start with the server action, then move on to the contact page, and finally the form component. | ||
|
||
## Server Action | ||
|
||
In our server action, we validate the form data using the Zod schema. If the validation fails, we return the array of errors. If the validation succeeds, we redirect the user to the home page. | ||
|
||
```ts | ||
// app/contact/server-action.ts | ||
house9 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
"use server"; | ||
|
||
import { redirect } from "next/navigation"; | ||
import { z } from "zod"; | ||
|
||
const schema = z.object({ | ||
name: z.string().min(1, "Name cannot be blank"), | ||
email: z.string().email().min(1, "Email cannot be blank"), | ||
}); | ||
|
||
export default async function contactAction(_prevState: any, params: FormData) { | ||
const validation = schema.safeParse({ | ||
name: params.get("name"), | ||
email: params.get("email"), | ||
}); | ||
|
||
if (validation.success) { | ||
// save the data, send an email, etc. | ||
redirect("/"); | ||
} else { | ||
return { | ||
errors: validation.error.issues, | ||
}; | ||
} | ||
} | ||
``` | ||
|
||
## Contact Page | ||
|
||
In our contact page, we import the server action and pass it to the `Form` component. The `ContactPage` is a [server component](https://nextjs.org/docs/app/building-your-application/rendering/server-components) while the `Form` component is a [client component](https://nextjs.org/docs/app/building-your-application/rendering/client-components). We need to use the `useFormState` hook to handle validation errors coming from the server. The `useFormState` hook can only be used in client components. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Love the links here. Very handy. |
||
|
||
```tsx | ||
// app/contact/page.tsx | ||
|
||
import contactAction from "./server-action"; | ||
import Form from "./form"; | ||
|
||
export default async function ContactPage() { | ||
return ( | ||
<> | ||
<h1 className="text-2xl font-bold mb-3">Contact Us</h1> | ||
<Form action={contactAction}></Form> | ||
</> | ||
); | ||
} | ||
``` | ||
|
||
## Form Component | ||
|
||
In our form component, we'll use the `useFormState` hook to handle validation errors. We can then match the `path` node of each error object in the form state to its corresponding input field, and extract all the `message` nodes to build an error message for each field. | ||
|
||
```tsx | ||
// app/contact/form.tsx | ||
house9 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
"use client"; | ||
|
||
import { useFormState, useFormStatus } from "react-dom"; | ||
import type { ZodIssue } from "zod"; | ||
|
||
type Props = { | ||
action: ( | ||
_prevState: any, | ||
params: FormData | ||
) => Promise<{ errors: ZodIssue[] }>; | ||
}; | ||
Comment on lines
+114
to
+119
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As a reader, I'd like to know what this type definition is doing. Can you explain? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Going to assume the reader is familiar with typescript already and do not want to tangent from the main concepts too much. |
||
|
||
export default function Form({ action }: Props) { | ||
const [state, formAction] = useFormState(action, { errors: [] }); | ||
|
||
const nameErrors = findErrors("name", state.errors); | ||
const emailErrors = findErrors("email", state.errors); | ||
|
||
return ( | ||
<form action={formAction}> | ||
<div> | ||
<label htmlFor="name"> | ||
<span>Name</span> | ||
<input id="name" type="text" name="name" /> | ||
</label> | ||
<ErrorMessages errors={nameErrors} /> | ||
</div> | ||
|
||
<div> | ||
<label htmlFor="email"> | ||
<span>Email</span> | ||
<input id="email" type="text" name="email" /> | ||
</label> | ||
<ErrorMessages errors={emailErrors} /> | ||
</div> | ||
|
||
<button type="submit">Submit</button> | ||
</form> | ||
); | ||
} | ||
|
||
const ErrorMessages = ({ errors }: { errors: string[] }) => { | ||
if (errors.length === 0) return null; | ||
|
||
const text = errors.join(", "); | ||
|
||
return <div className="text-red-600 peer">{text}</div>; | ||
}; | ||
|
||
const findErrors = (fieldName: string, errors: ZodIssue[]) => { | ||
return errors | ||
.filter((item) => { | ||
return item.path.includes(fieldName); | ||
}) | ||
.map((item) => item.message); | ||
}; | ||
``` | ||
|
||
## Wrapping it Up | ||
|
||
Zod provides a powerful and flexible way to handle form validation in Next.js applications. Using Zod, you can implement server-side validation to our forms and still present a great user experience by following a few simple steps: | ||
|
||
- Define a Zod schema for each server action | ||
- Validate user input in your server action using the Zod schema | ||
- Return an array of errors if validation fails | ||
- In your client component, use the `useFormState` hook to receive validation errors | ||
- Display the validation errors to the user | ||
|
||
Now go build some great forms! |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
@workspace tell me about the contact page | ||
|
||
--- | ||
|
||
As an expert javascript/typescript and nextjs expert software developer | ||
with technical writing skills, can you help me write a blog post using this code workspace? | ||
|
||
Objective: using Zod for form validation with react server actions in Nextjs | ||
|
||
some details worth including: | ||
|
||
- server side validation is a must, even if you agument with client side validation | ||
- overview of zod schema for validation | ||
- note that a 'required' field is accomplished using `min(1)` | ||
- validation is returned from the server action as a response and consumed in the client component using `useFormState` hook | ||
- the server response is an array of errors (`ZodIssue[]`), each item has the below structure | ||
|
||
```json | ||
{ | ||
"code": "too_small", | ||
"minimum": 1, | ||
"type": "string", | ||
"inclusive": true, | ||
"exact": false, | ||
"message": "Name cannot be blank", | ||
"path": ["name"] | ||
} | ||
``` | ||
|
||
- we match the `path` node to each input field and extract all `message` nodes to build the error message | ||
|
||
-- after first post | ||
|
||
Can you rewrite that blog post but after the example ZodIssue example json I want to include the full code for contact directory; server action, followed by the contact page and then the form component, explain as much of each as needed | ||
|
||
-- did not go well, was not writting proper code but also was not explicity saying it was referencing anything except this file | ||
|
||
-- added code manually |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a fabulous introduction 👏🏽