Next.js 是一个强大的全栈框架,允许我们构建具有前端和后端功能的应用程序。它非常灵活,可用于从简单的静态网站到复杂的 Web 应用程序的所有内容。今天,我们将使用 Next.js 构建电子邮件联系表单。
表单是任何网站的关键部分,让用户与应用程序进行交互。无论是注册、登录、提供反馈还是收集数据,表单对于用户体验都至关重要。如果没有表单,全栈应用程序将无法正确收集和处理用户输入。
在本博客中,我将向您展示如何使用 Next.js、Resend 和 Zod(用于表单验证)创建电子邮件联系表单。我们将介绍设置项目、设计表单、处理表单提交以及创建单独的 API 路径。最后,您将了解如何构建表单并将其添加到您的 Next.js 应用程序,确保您的 Web 应用程序运行良好且易于使用。
那么,事不宜迟,让我们开始吧。
Resend 是面向开发人员的现代电子邮件 API。它旨在使从您的应用程序发送电子邮件变得简单、可靠且可扩展。与传统电子邮件服务不同,Resend 专为开发人员而构建,提供简单的 API,可与各种编程语言和框架(包括 Next.js)无缝集成。
在我们的 Next.js 表单项目中,我们将使用 Resend 发送电子邮件。当用户提交表单时,我们将使用 Resend 的 API 发送确认电子邮件或根据需要处理表单数据。
Zod 是一个强大的数据工具。它是一个 TypeScript 优先的库,可以帮助您定义和检查数据的形状。将其视为为数据设置规则,然后在使用数据之前确保数据符合这些规则。
如果你正在使用 TypeScript(如果你没有,你应该考虑它!),Zod 可以很好地使用它。它可以自动从您的模式推断 TypeScript 类型,这可以节省大量时间。 TypeScript 在编译时检查类型,而 Zod 在运行时检查类型。这意味着您可以捕获可能通过静态类型检查的数据问题。您可以将 Zod 用于各种数据验证场景,从简单的表单输入到复杂的 API 响应。
让我们首先设置具有所有必要依赖项的 Next.js 项目。我们将使用 TypeScript 实现类型安全,使用 Tailwind CSS 进行样式设计,使用 Ant Design 进行 UI 组件,使用 Zod 进行表单验证,并使用 Resend 进行电子邮件功能。
npx create-next-app@latest my-contact-form --typescript cd my-contact-form
yarn add antd zod resend react-icons
为了发送电子邮件,我们将使用重新发送,因此我们需要重新发送 API 密钥。在启动我们的服务器之前,让我们转到重新发送并获取我们的 API 密钥。单击此处转到重新发送站点,然后单击登录按钮。
登录后,您将被重定向到此页面。在这里,您将看到从表单收到的所有电子邮件。
在这里,单击 API 密钥部分
然后,点击此生成 API 密钥?按钮
现在,复制该 API 密钥并妥善保管。接下来,打开 VSCode 并在根文件夹中创建一个名为 .env 的新文件。在那里添加一个环境变量。
RESEND_API_KEY=yourapikeywillbehere
现在您还可以使用此命令运行服务器。
yarn dev
让我们从创建电子邮件模板开始。这将是当有人通过联系表单向您发送电子邮件时您收到的模板。
import * as React from 'react'; interface EmailTemplateProps { firstName: string; message: string; } export const EmailTemplate: React.FC<EmailTemplateProps> = ({ firstName, message, }) => ( <div> <h1>Hello, I am {firstName}!</h1> <p>You have received a new message from your Portfolio:</p> <p>{message}</p> </div> );
这个简单的 React 组件定义了当有人提交联系表单时将发送的电子邮件的结构。它需要两个属性:firstName 和 message。该组件使用名字创建个性化问候语并显示提交的消息。
这里。我们将了解如何使用 Resend API 实现电子邮件发送功能。
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 <onboarding@resend.dev>', to: 'katare27451@gmail.com', 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 }); } }
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:
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 <onboarding@resend.dev>', to: 'katare27451@gmail.com', 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:
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 ?
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
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';
Now, let's create our UI, and then we'll move on to the logic. Our form will look something like this.?
In your page.tsx, after all the import statements, define a component and add a return statement first.
const ContactPage: React.FC = () => { return ( <div className="max-w-3xl w-full space-y-8 bg-white p-10 rounded-xl shadow-2xl"> /* our code will be here */ </div> ); }; export default ContactPage;
Currently, we have just a simple div with a few tailwind styling now, we'll first add our heading part.
... <div> <h2 className="mt-1 text-center text-3xl font-extrabold text-gray-900">Get in Touch</h2> <p className="mt-1 mb-4 text-center text-sm text-gray-600"> I'd love to hear from you! Fill out the form below to get in touch. </p> </div> ...
Now, let's add our input fields
... <Form form={form} name="contact" onFinish={onFinish} layout="vertical" className="mt-8 space-y-6" > <Form.Item name="name" rules={[{ required: true, message: 'Please input your name!' }]} > <Input prefix={<FaUser className="site-form-item-icon" />} placeholder="Your Name" size="large" /> </Form.Item> <Form.Item name="email" rules={[ { required: true, message: 'Please input your email!' }, { type: 'email', message: 'Please enter a valid email!' } ]} > <Input prefix={<MdMail className="site-form-item-icon" />} placeholder="Your Email" size="large" /> </Form.Item> <Form.Item name="subject" rules={[{ required: true, message: 'Please input a subject!' }]} > <Input prefix={<FaMessage className="site-form-item-icon" />} placeholder="Subject" size="large" /> </Form.Item> <Form.Item name="message" rules={[{ required: true, message: 'Please input your message!' }]} > <TextArea placeholder="Your Message" autoSize={{ minRows: 4, maxRows: 8 }} /> </Form.Item> <Form.Item> <Button type="primary" htmlType="submit" className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" disabled={isSubmitting} > {isSubmitting ? 'Sending...' : 'Send Message'} </Button> </Form.Item> </Form> ...
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 form={form} name="contact" onFinish={onFinish} layout="vertical" className="mt-8 space-y-6" > {/* Form items */} </Form>
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.
<Form.Item name="name" rules={[{ required: true, message: 'Please input your name!' }]} > <Input prefix={<FaUser className="site-form-item-icon" />} placeholder="Your Name" size="large" /> </Form.Item>
Similarly, we have Email and other fields.
<Form.Item name="email" rules={[ { required: true, message: 'Please input your email!' }, { type: 'email', message: 'Please enter a valid email!' } ]} > <Input prefix={<MdMail className="site-form-item-icon" />} placeholder="Your Email" size="large" /> </Form.Item>
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
<Form.Item> <Button type="primary" htmlType="submit" className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" disabled={isSubmitting} > {isSubmitting ? 'Sending...' : 'Send Message'} </Button> </Form.Item>
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.
So, in the logic part, we have a few things to do:
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<typeof contactSchema>;
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:
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<ContactFormData>(); 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:
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<typeof contactSchema>; const ContactPage: React.FC = () => { const [form] = Form.useForm<ContactFormData>(); 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 (); }; export default ContactPage;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" />
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.
Now, let's fill out the form
and hit the send button. It's in process.
Here we go, the message is sent. The sender receives a notification saying "Message sent," and the form is also refreshed.
The receiver also gets the message?
And that's it. We have successfully built an email contact form in Next.js using Resend and Zod.
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 :)
GitHub
以上是如何使用 Resend 和 Zod 在 Next.js 中创建动态电子邮件联系表单的详细内容。更多信息请关注PHP中文网其他相关文章!