Skip to content

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

Merged
merged 7 commits into from
Feb 2, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 7 additions & 30 deletions README.md
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.
1 change: 1 addition & 0 deletions app/contact/form.tsx
Original file line number Diff line number Diff line change
@@ -19,6 +19,7 @@ type Props = {

export default function Form({ action, config }: Props) {
const [state, formAction] = useFormState(action, { errors: [] });
console.log(state);
const findErrors = useCallback(
(fieldName: string) => {
return state.errors
2 changes: 1 addition & 1 deletion app/contact/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import contactAction, { getConfig } from './server-action'
import Form from './form'

export default async function Contact() {
export default async function ContactPage() {
const config = await getConfig()

return (
1 change: 1 addition & 0 deletions app/contact/server-action.ts
Original file line number Diff line number Diff line change
@@ -23,6 +23,7 @@ export default async function contactAction(_prevState: any, params: FormData) {
})

if (validation.success) {
// save the data, send an email, etc.
console.log(validation.data)
redirect('/')
} else {
4 changes: 2 additions & 2 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -18,7 +18,7 @@ export default function RootLayout({
<body>
<main className="flex flex-col items-center w-full">
<nav className='bg-green-400 p-4 flex justify-between items-center w-full'>
<h1 className='text-black font-bold text-2xl tracking-wide'>mister deejay</h1>
<a className='text-black font-bold text-2xl tracking-wide' href="/">mister deejay</a>
<ul className='flex justify-end gap-8 text-black font-semibold'>
<li><Link href="/">Home</Link></li>
<li><Link href="/contact">Contact</Link></li>
@@ -32,7 +32,7 @@ export default function RootLayout({
<div className='flex flex-col items-center md:flex-row md:justify-center'>
<small className='text-sm text-center md:text-left'>Copyright © {new Date().getFullYear()}. <a href='https://codingzeal.com' target='_blank' className='text-green-700 underline font-semibold hover:no-underline'>ZEAL</a>. All Rights Reserved.</small>
<small className='text-sm hidden md:block'>&nbsp;&nbsp;|&nbsp;&nbsp;</small>
<small><a href='#' target='_blank' className='text-sm text-green-700 underline font-semibold hover:no-underline'>Source Code</a></small>
<small><a href='https://github.com/CodingZeal/nextjs-server-action-validation' target='_blank' className='text-sm text-green-700 underline font-semibold hover:no-underline'>Source Code</a></small>
</div>
</footer>
</body>
177 changes: 177 additions & 0 deletions post.md
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.
Copy link
Contributor

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 👏🏽


## 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"),
});
```

## 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:
Copy link
Contributor

@sarmstead sarmstead Jan 16, 2024

Choose a reason for hiding this comment

The 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 ...."

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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

"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.
Copy link
Contributor

Choose a reason for hiding this comment

The 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

"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
Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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!
38 changes: 38 additions & 0 deletions prompt.md
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