”工欲善其事,必先利其器。“—孔子《论语.录灵公》
首页 > 编程 > 如何使用 Resend 和 Zod 在 Next.js 中创建动态电子邮件联系表单

如何使用 Resend 和 Zod 在 Next.js 中创建动态电子邮件联系表单

发布于2024-08-16
浏览:628

Introduction

Next.js is a powerful full-stack framework that allows us to build applications with both frontend and backend features. It's very flexible and can be used for everything from simple static websites to complex web apps. Today, we will use Next.js to build an email contact form.

Forms are a key part of any website, letting users interact with the application. Whether it's for signing up, logging in, giving feedback, or collecting data, forms are vital for user experience. Without forms, a full-stack application wouldn't be able to gather and process user input properly.

In this blog, I will show you how to create an email contact form using Next.js, Resend, and Zod (for form validation). We will cover setting up the project, designing the form, handling form submissions, and creating a separate API route. By the end, you will know how to build and add forms to your Next.js apps, ensuring your web app works well and is easy to use.

So, without further delay, let's get started.

What is Resend?

Resend is a modern email API for developers. It's designed to make sending emails from your applications simple, reliable, and scalable. Unlike traditional email services, Resend is built with developers in mind, offering a straightforward API that integrates seamlessly with various programming languages and frameworks, including Next.js.

In our Next.js form project, we'll use Resend to send emails. When a user submits the form, we'll use Resend's API to send a confirmation email or process the form data as needed.

What is Zod?

Zod is a powerful tool for your data. It's a TypeScript-first library that helps you define and check the shape of your data. Think of it as setting rules for your data and then making sure the data matches those rules before you use it.

How to Create Dynamic Email Contact Form in Next.js Using Resend and Zod

If you're using TypeScript (and if you're not, you should consider it!), Zod plays nicely with it. It can automatically infer TypeScript types from your schemas, which is a huge time-saver. While TypeScript checks types at compile-time, Zod does it at runtime. This means you can catch data issues that might slip through static type checking. You can use Zod for all sorts of data validation scenarios, from simple form inputs to complex API responses.

Project Setup

Let's start by setting up our Next.js project with all the necessary dependencies. We'll use TypeScript for type safety, Tailwind CSS for styling, Ant Design for UI components, Zod for form validation, and Resend for email functionality.

  • Create a new Next.js project with TypeScript:
npx create-next-app@latest my-contact-form --typescript
cd my-contact-form
  • Install additional dependencies:
yarn add antd zod resend react-icons

Setup Environment variables

For sending an email, we will use Resend, so we need the Resend API key. Before starting our server, let's go to Resend and get our API keys. Click here to go to the Resend site, and click the sign-in button.

How to Create Dynamic Email Contact Form in Next.js Using Resend and Zod

After signing in, you'll be redirected to this page. Here, you'll see all the emails you received from your form.

How to Create Dynamic Email Contact Form in Next.js Using Resend and Zod

Here, click on the API Keys section

How to Create Dynamic Email Contact Form in Next.js Using Resend and Zod

And, generate an API key by clicking on this ? button

How to Create Dynamic Email Contact Form in Next.js Using Resend and Zod

Now, copy that API key and keep it safe. Next, open VSCode and create a new file named .env in your root folder. Add an environment variable there.

RESEND_API_KEY=yourapikeywillbehere

Now you can also run your server using this command.

yarn dev

Email Template Component

Let's start by creating an email template. This will be the template you receive when someone sends you an email via the contact form.

import * as React from 'react';

interface EmailTemplateProps {
  firstName: string;
  message: string;
}

export const EmailTemplate: React.FC = ({
  firstName,
  message,
}) => (
  

Hello, I am {firstName}!

You have received a new message from your Portfolio:

{message}

);

This simple React component defines the structure of the email that will be sent when someone submits the contact form. It takes two props: firstName and message. The component creates a personalized greeting using the first name and displays the submitted message.

Implementing Email Sending with Resend API

Here. we'll see how to implement email-sending functionality using the Resend API.

The Code Structure

First, let's look at where this code lives in our Next.js project:

app/
  ├── api/
  │   └── v1/
  │       └── send/
  │           └── route.ts

This structure follows Next.js 13's App Router convention, where API routes are defined as route handlers within the app directory.

This is our complete API route code ?

import { EmailTemplate } from 'app/components/email-template';
import { NextResponse } from 'next/server';
import { Resend } from 'resend';
import { v4 as uuid } from 'uuid';

const resend = new Resend(process.env.RESEND_API_KEY);

export async function POST(req: Request) {
  try {
    const { name, email, subject, message } = await req.json();

    const { data, error } = await resend.emails.send({
      from: 'Contact Form ',
      to: '[email protected]',
      subject: subject || 'New Contact Form Submission',
      reply_to: email,
      headers: {
        'X-Entity-Ref-ID': uuid(),
      },
      react: EmailTemplate({ firstName: name, message }) as React.ReactElement,
    });

    if (error) {
      return NextResponse.json({ error }, { status: 500 });
    }

    return NextResponse.json({ data, message: 'success' }, { status: 200 });
  } catch (error) {
    console.error('Error processing request:', error);
    return NextResponse.json({ error: 'Failed to process request' }, { status: 500 });
  }
}

Breaking Down the Code

Now, let's examine each part of the code:

import { EmailTemplate } from 'app/components/email-template';
import { NextResponse } from 'next/server';
import { Resend } from 'resend';
import { v4 as uuid } from 'uuid';

These import statements bring in the necessary dependencies:

  • EmailTemplate: A custom React component for our email content(That we already built above.
  • NextResponse: Next.js utility for creating API responses.
  • Resend: The Resend API client.
  • uuid: For generating unique identifiers.
const resend = new Resend(process.env.RESEND_API_KEY);

Here, we initialize the Resend client with our API key. It's crucial to keep this key secret, so we store it in an environment variable.

export async function POST(req: Request) {
  // ... (code inside this function)
}

This exports an async function named POST, which Next.js will automatically use to handle POST requests to this route.

const { name, email, subject, message } = await req.json();

We extract the form data from the request body. This assumes the client is sending a JSON payload with these fields.

const { data, error } = await resend.emails.send({
  from: 'Contact Form ',
  to: '[email protected]',
  subject: subject || 'New Contact Form Submission',
  reply_to: email,
  headers: {
    'X-Entity-Ref-ID': uuid(),
  },
  react: EmailTemplate({ firstName: name, message }) as React.ReactElement,
});

This is where we'll get our emails! We use Resend's send method to dispatch the email:

  • from: The sender's email address.
  • to: The recipient's email address.
  • subject: The email subject, using the provided subject or a default.
  • reply_to: Sets the reply-to address to the form submitter's email.
  • headers: Includes a unique ID for tracking.
  • react: Uses our custom EmailTemplate component to generate the email content.
if (error) {
  return NextResponse.json({ error }, { status: 500 });
}

return NextResponse.json({ data, message: 'success' }, { status: 200 });

Here, we handle the response from Resend. If there's an error, we return a 500 status with the error details. Otherwise, we send a success response.

catch (error) {
  console.error('Error processing request:', error);
  return NextResponse.json({ error: 'Failed to process request' }, { status: 500 });
}

This catch block handles any unexpected errors, logs them, and returns a generic error response.

And that's it! We've set up our API route. The only thing left is to set up our logic and UI. Let's do that too ?

Contact Page Component

In your app directory, create a new folder named contact-form and inside this folder, create a file named page.tsx.

app/
  ├── contact-form/
  │   └── page.tsx

Imports and Dependencies

First, import all necessary components from Ant Design, Next.js, and React Icons. We also import Zod for form validation.

import React from 'react';
import { Form, Input, Button, message, Space, Divider, Typography } from 'antd';
import Head from 'next/head';
import { FaUser } from 'react-icons/fa';
import { MdMail } from 'react-icons/md';
import { FaMessage } from 'react-icons/fa6';
import { z } from 'zod';
import Paragraph from 'antd/es/typography/Paragraph';

UI Layout and Design

Now, let's create our UI, and then we'll move on to the logic. Our form will look something like this.?

How to Create Dynamic Email Contact Form in Next.js Using Resend and Zod

In your page.tsx, after all the import statements, define a component and add a return statement first.

const ContactPage: React.FC = () => {
  return (
      
/* our code will be here */
); }; export default ContactPage;

Currently, we have just a simple div with a few tailwind styling now, we'll first add our heading part.

How to Create Dynamic Email Contact Form in Next.js Using Resend and Zod

...

        

Get in Touch

I'd love to hear from you! Fill out the form below to get in touch.

...

Now, let's add our input fields

How to Create Dynamic Email Contact Form in Next.js Using Resend and Zod

 ...     
        
} placeholder="Your Name" size="large" /> } placeholder="Your Email" size="large" /> } placeholder="Subject" size="large" /> ...

Here, in the above code firstly we added a Form Component. This is the main Form component from Ant Design. It has the following props:

{/* Form items */}
  • form: Links the form to the form object created using Form.useForm().
  • name: Gives the form a name, in this case, "contact".
  • onFinish(we'll declare this function in our next section): Specifies the function to be called when the form is submitted successfully.
  • layout: Sets the form layout to "vertical".
  • className: Applies CSS classes for styling.

Then, we added a Form Items. Each Form.Item represents a field in the form. Let's look at the "name" field as an example.


  } placeholder="Your Name" size="large" />





  • name: Specifies the field name.
  • rules: An array of validation rules. Here, it's set as required.
  • The Input component is used for text input, with a user icon prefix and a placeholder.

Similarly, we have Email and other fields.


  } placeholder="Your Email" size="large" />





This field has an additional rule to ensure the input is a valid email address.

Subject and Message Fields: These are similar to the name field, with the message field using a TextArea component for multi-line input.

Then, we have a Submit Button to submit our form


  





This is the submit button for the form. It's disabled when isSubmitting (we'll add this state in our next section) is true, and its text changes to "Sending..." during submission.

Form Submission Logic

So, in the logic part, we have a few things to do:

  • Setting up Zod schema for form validation
  • Adding new states
  • and, implementing a onFinish function

We'll start with setting up our schema first.

// Zod schema for form validation
const contactSchema = z.object({
  name: z.string().min(4, 'Name must be at least 4 characters').max(50, 'Name must not exceed 50 characters'),
  email: z.string().email('Invalid email address').regex(/^[\w-\.] @([\w-] \.) [\w-]{2,4}$/, "Email must be a valid format"),
  subject: z.string().min(5, 'Subject must be at least 5 characters').max(100, 'Subject must not exceed 100 characters'),
  message: z.string().min(20, 'Message must be at least 20 characters').max(1000, 'Message must not exceed 1000 characters'),
});

type ContactFormData = z.infer;

This part defines a Zod schema for form validation. As we already learned, Zod is a TypeScript-first schema declaration and validation library. The contactSchema object defines the structure and validation rules for the contact form:

  • name: Must be a string between 4 and 50 characters.
  • email: Must be a valid email address and match the specified regex pattern.
  • subject: Must be a string between 5 and 100 characters.
  • message: Must be a string between 20 and 1000 characters.

The ContactFormData type is inferred from the Zod schema, providing type safety for the form data.

Now, let's add 2 new states and implement our onFinish function.

const ContactPage: React.FC = () => {
  const [form] = Form.useForm();
  const [isSubmitting, setIsSubmitting] = React.useState(false);

  const onFinish = async (values: ContactFormData) => {
    setIsSubmitting(true);
    try {
      contactSchema.parse(values);

      const response = await fetch('/api/v1/send', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(values),
      });

      if (!response.ok) {
        message.error('Failed to send message. Please try again.');
        setIsSubmitting(false); 
      }

      const data = await response.json();

      if (data.message === 'success') {
        message.success('Message sent successfully!');
        setIsSubmitting(false);
        form.resetFields();
      } else {
        throw new Error('Failed to send message');
      }
    } catch (error) {
      if (error instanceof z.ZodError) {
        error.errors.forEach((err) => {
          message.error(err.message);
          setIsSubmitting(false);
        });
      } else {
        message.error('Failed to send message. Please try again.');
        setIsSubmitting(false);
      }
    } finally {
      setIsSubmitting(false);
    }
  };

This part defines the ContactPage functional component:

  • It uses the Form.useForm hook to create a form instance.
  • It manages a isSubmitting state to track form submission status.
  • The onFinish function is called when the form is submitted:
  1. It sets isSubmitting to true.
  2. It uses contactSchema.parse(values) to validate the form data against the Zod schema.
  3. If validation passes, it sends a POST request to /api/v1/send with the form data.
  4. It handles the response, showing success or error messages accordingly.
  5. If there's a Zod validation error, it displays the error message.
  6. Finally, it sets isSubmitting back to false.

This setup ensures that the form data is validated on both the client-side (using Antd's form validation) and the server-side (using Zod schema validation) before sending the data to the server. It also manages the submission state and provides user feedback through success or error messages.

And, this is the complete code of our contact-form file ?

"use client";

import React from 'react';
import { Form, Input, Button, message, Space, Divider, Typography } from 'antd';
import Head from 'next/head';
import { FaUser } from 'react-icons/fa';
import { MdMail } from 'react-icons/md';
import { FaMessage } from 'react-icons/fa6';
import { z } from 'zod';
import { Container } from 'app/components/container';
import Paragraph from 'antd/es/typography/Paragraph';

const { TextArea } = Input;
const { Text } = Typography;

// Zod schema for form validation
const contactSchema = z.object({
  name: z.string().min(4, 'Name must be at least 4 characters').max(50, 'Name must not exceed 50 characters'),
  email: z.string().email('Invalid email address').regex(/^[\w-\.] @([\w-] \.) [\w-]{2,4}$/, "Email must be a valid format"),
  subject: z.string().min(5, 'Subject must be at least 5 characters').max(100, 'Subject must not exceed 100 characters'),
  message: z.string().min(20, 'Message must be at least 20 characters').max(1000, 'Message must not exceed 1000 characters'),
});

type ContactFormData = z.infer;

const ContactPage: React.FC = () => {
  const [form] = Form.useForm();
  const [isSubmitting, setIsSubmitting] = React.useState(false);

  const onFinish = async (values: ContactFormData) => {
    setIsSubmitting(true);
    try {
      contactSchema.parse(values);

      const response = await fetch('/api/v1/send', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(values),
      });

      if (!response.ok) {
        message.error('Failed to send message. Please try again.');
        setIsSubmitting(false); 
      }

      const data = await response.json();

      if (data.message === 'success') {
        message.success('Message sent successfully!');
        setIsSubmitting(false);
        form.resetFields();
      } else {
        throw new Error('Failed to send message');
      }
    } catch (error) {
      if (error instanceof z.ZodError) {
        error.errors.forEach((err) => {
          message.error(err.message);
          setIsSubmitting(false);
        });
      } else {
        message.error('Failed to send message. Please try again.');
        setIsSubmitting(false);
      }
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
      

Get in Touch

I'd love to hear from you! Fill out the form below to get in touch.

} placeholder="Your Name" size="large" /> } placeholder="Your Email" size="large" /> } placeholder="Subject" size="large" />
); }; export default ContactPage;

Testing

Till now, we're all set, now it's time to run and test our application.

Start your server:

yarn dev

First, let's try to hit the endpoint without filling out the form. As expected, the API doesn't get called, and we receive error messages.

How to Create Dynamic Email Contact Form in Next.js Using Resend and Zod

Now, let's fill out the form

How to Create Dynamic Email Contact Form in Next.js Using Resend and Zod

and hit the send button. It's in process.

How to Create Dynamic Email Contact Form in Next.js Using Resend and Zod

Here we go, the message is sent. The sender receives a notification saying "Message sent," and the form is also refreshed.

How to Create Dynamic Email Contact Form in Next.js Using Resend and Zod

The receiver also gets the message?

How to Create Dynamic Email Contact Form in Next.js Using Resend and Zod

How to Create Dynamic Email Contact Form in Next.js Using Resend and Zod

And that's it. We have successfully built an email contact form in Next.js using Resend and Zod.

Conclusion

In this article, we built a contact form using Next.js and implemented features like form validation with Zod and email functionality with Resend. We start by setting up the Next.js project, configuring necessary dependencies, and managing environment variables for secure API access.

Then, we designed the email template, set up an API route for handling email submissions, and implemented the frontend form with Ant Design components.

If you want to see a live preview of it, you can check it out here. I have implemented the same form in my portfolio.

Thanks for reading this blog. If you learned something from it, please like and share it with your friends and community. I write blogs and share content on JavaScript, TypeScript, Open Source, and other web development-related topics. Feel free to follow me on my socials. I'll see you in the next one. Thank You :)

Twitter

LinkedIn

GitHub

版本声明 本文转载于:https://dev.to/shivamkatare/how-to-create-dynamic-email-contact-form-in-nextjs-using-resend-and-zod-50d?1如有侵犯,请联系[email protected]删除
最新教程 更多>
  • 如何将PANDAS DataFrame列转换为DateTime格式并按日期过滤?
    如何将PANDAS DataFrame列转换为DateTime格式并按日期过滤?
    将pandas dataframe列转换为dateTime格式示例:使用column(mycol)包含以下格式的以下dataframe,以自定义格式:})指定的格式参数匹配给定的字符串格式。转换后,MyCol列现在将包含DateTime对象。 date oped filtering > = p...
    编程 发布于2025-07-10
  • 如何使用PHP将斑点(图像)正确插入MySQL?
    如何使用PHP将斑点(图像)正确插入MySQL?
    essue VALUES('$this->image_id','file_get_contents($tmp_image)')";This code builds a string in PHP, but the function call ...
    编程 发布于2025-07-10
  • 切换到MySQLi后CodeIgniter连接MySQL数据库失败原因
    切换到MySQLi后CodeIgniter连接MySQL数据库失败原因
    无法连接到mySQL数据库:故障排除错误消息要调试问题,建议将以下代码添加到文件的末尾.//config/database.php并查看输出: ... ... 回声'... echo '<pre>'; print_r($db['default']); echo '</pr...
    编程 发布于2025-07-10
  • 如何同步迭代并从PHP中的两个等级阵列打印值?
    如何同步迭代并从PHP中的两个等级阵列打印值?
    同步的迭代和打印值来自相同大小的两个数组使用两个数组相等大小的selectbox时,一个包含country代码的数组,另一个包含乡村代码,另一个包含其相应名称的数组,可能会因不当提供了exply for for for the uncore for the forsion for for ytry...
    编程 发布于2025-07-10
  • 如何在鼠标单击时编程选择DIV中的所有文本?
    如何在鼠标单击时编程选择DIV中的所有文本?
    在鼠标上选择div文本单击带有文本内容,用户如何使用单个鼠标单击单击div中的整个文本?这允许用户轻松拖放所选的文本或直接复制它。 在单个鼠标上单击的div元素中选择文本,您可以使用以下Javascript函数: function selecttext(canduterid){ if(do...
    编程 发布于2025-07-10
  • 如何使用node-mysql在单个查询中执行多个SQL语句?
    如何使用node-mysql在单个查询中执行多个SQL语句?
    Multi-Statement Query Support in Node-MySQLIn Node.js, the question arises when executing multiple SQL statements in a single query using the node-mys...
    编程 发布于2025-07-10
  • 为什么使用固定定位时,为什么具有100%网格板柱的网格超越身体?
    为什么使用固定定位时,为什么具有100%网格板柱的网格超越身体?
    网格超过身体,用100%grid-template-columns 为什么在grid-template-colms中具有100%的显示器,当位置设置为设置的位置时,grid-template-colly修复了?问题: 考虑以下CSS和html: class =“ snippet-code”> g...
    编程 发布于2025-07-10
  • 在PHP中如何高效检测空数组?
    在PHP中如何高效检测空数组?
    在PHP 中检查一个空数组可以通过各种方法在PHP中确定一个空数组。如果需要验证任何数组元素的存在,则PHP的松散键入允许对数组本身进行直接评估:一种更严格的方法涉及使用count()函数: if(count(count($ playerList)=== 0){ //列表为空。 } 对...
    编程 发布于2025-07-10
  • 为什么Microsoft Visual C ++无法正确实现两台模板的实例?
    为什么Microsoft Visual C ++无法正确实现两台模板的实例?
    The Mystery of "Broken" Two-Phase Template Instantiation in Microsoft Visual C Problem Statement:Users commonly express concerns that Micro...
    编程 发布于2025-07-10
  • Python中何时用"try"而非"if"检测变量值?
    Python中何时用"try"而非"if"检测变量值?
    使用“ try“ vs.” if”来测试python 在python中的变量值,在某些情况下,您可能需要在处理之前检查变量是否具有值。在使用“如果”或“ try”构建体之间决定。“ if” constructs result = function() 如果结果: 对于结果: ...
    编程 发布于2025-07-10
  • 如何在无序集合中为元组实现通用哈希功能?
    如何在无序集合中为元组实现通用哈希功能?
    在未订购的集合中的元素要纠正此问题,一种方法是手动为特定元组类型定义哈希函数,例如: template template template 。 struct std :: hash { size_t operator()(std :: tuple const&tuple)const {...
    编程 发布于2025-07-10
  • Java数组中元素位置查找技巧
    Java数组中元素位置查找技巧
    在Java数组中检索元素的位置 利用Java的反射API将数组转换为列表中,允许您使用indexof方法。 (primitives)(链接到Mishax的解决方案) 用于排序阵列的数组此方法此方法返回元素的索引,如果发现了元素的索引,或一个负值,指示应放置元素的插入点。
    编程 发布于2025-07-10
  • 如何有效地选择熊猫数据框中的列?
    如何有效地选择熊猫数据框中的列?
    在处理数据操作任务时,在Pandas DataFrames 中选择列时,选择特定列的必要条件是必要的。在Pandas中,选择列的各种选项。选项1:使用列名 如果已知列索引,请使用ILOC函数选择它们。请注意,python索引基于零。 df1 = df.iloc [:,0:2]#使用索引0和1 c...
    编程 发布于2025-07-10
  • 为什么PYTZ最初显示出意外的时区偏移?
    为什么PYTZ最初显示出意外的时区偏移?
    与pytz 最初从pytz获得特定的偏移。例如,亚洲/hong_kong最初显示一个七个小时37分钟的偏移: 差异源利用本地化将时区分配给日期,使用了适当的时区名称和偏移量。但是,直接使用DateTime构造器分配时区不允许进行正确的调整。 example pytz.timezone(...
    编程 发布于2025-07-10
  • 如何在Chrome中居中选择框文本?
    如何在Chrome中居中选择框文本?
    选择框的文本对齐:局部chrome-inly-ly-ly-lyly solument 您可能希望将文本中心集中在选择框中,以获取优化的原因或提高可访问性。但是,在CSS中的选择元素中手动添加一个文本 - 对属性可能无法正常工作。初始尝试 state)</option> < op...
    编程 发布于2025-07-10

免责声明: 提供的所有资源部分来自互联网,如果有侵犯您的版权或其他权益,请说明详细缘由并提供版权或权益证明然后发到邮箱:[email protected] 我们会第一时间内为您处理。

Copyright© 2022 湘ICP备2022001581号-3