Resend と Zod を使用して Next.js で動的なメール連絡フォームを作成する方法

PHPz
リリース: 2024-08-10 18:43:32
オリジナル
346 人が閲覧しました

導入

Next.js は、フロントエンドとバックエンドの両方の機能を備えたアプリケーションを構築できる強力なフルスタック フレームワークです。非常に柔軟性があり、単純な静的 Web サイトから複雑な Web アプリまであらゆるものに使用できます。今日は、Next.js を使用して電子メールお問い合わせフォームを構築します。

フォームは Web サイトの重要な部分であり、ユーザーがアプリケーションを操作できるようになります。サインアップ、ログイン、フィードバックの提供、データ収集のいずれの場合でも、フォームはユーザー エクスペリエンスにとって不可欠です。フォームがなければ、フルスタック アプリケーションはユーザー入力を適切に収集して処理できません。

このブログでは、Next.js、Resend、および Zod (フォーム検証用) を使用して電子メール コンタクト フォームを作成する方法を説明します。プロジェクトのセットアップ、フォームの設計、フォーム送信の処理、別の API ルートの作成について説明します。最後には、Next.js アプリを構築してフォームを追加し、Web アプリが適切に動作し、使いやすくなる方法を理解できるようになります。

それでは、早速始めましょう。

再送信とは何ですか?

Resend は、開発者向けの最新のメール API です。アプリケーションからの電子メール送信をシンプル、信頼性、拡張性に優れたものにするように設計されています。従来の電子メール サービスとは異なり、Resend は開発者を念頭に置いて構築されており、Next.js を含むさまざまなプログラミング言語やフレームワークとシームレスに統合する簡単な API を提供します。

Next.js フォーム プロジェクトでは、Resend を使用して電子メールを送信します。ユーザーがフォームを送信すると、Resend の API を使用して確認メールを送信するか、必要に応じてフォーム データを処理します。

ゾッドとは何ですか?

Zod はデータのための強力なツールです。これは、データの形状を定義および確認するのに役立つ TypeScript ファーストのライブラリです。これは、データにルールを設定し、データを使用する前にそのルールに一致することを確認することだと考えてください。

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

TypeScript を使用している場合 (使用していない場合は、検討する必要があります!)、Zod は TypeScript とうまく連携します。スキーマから TypeScript 型を自動的に推論できるため、時間を大幅に節約できます。 TypeScript はコンパイル時に型をチェックしますが、Zod は実行時に型をチェックします。これは、静的型チェックをすり抜けてしまう可能性のあるデータの問題をキャッチできることを意味します。 Zod は、単純なフォーム入力から複雑な API 応答まで、あらゆる種類のデータ検証シナリオに使用できます。

プロジェクトのセットアップ

必要な依存関係をすべて備えた Next.js プロジェクトをセットアップすることから始めましょう。タイプ セーフティには TypeScript、スタイル設定には Tailwind CSS、UI コンポーネントには Ant Design、フォーム検証には Zod、電子メール機能には Resend を使用します。

  • TypeScript を使用して新しい Next.js プロジェクトを作成します。
npx create-next-app@latest my-contact-form --typescript
cd my-contact-form
ログイン後にコピー
  • 追加の依存関係をインストールします。
yarn add antd zod resend react-icons
ログイン後にコピー

環境変数のセットアップ

メールの送信には再送信を使用するため、再送信 API キーが必要です。サーバーを起動する前に、「再送信」に移動して API キーを取得しましょう。ここをクリックして再送信サイトに移動し、サインイン ボタンをクリックします。

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

ここで、「API キー」セクションをクリックします

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

そして、これをクリックして API キーを生成しますか?ボタン

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

次に、その 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 という 2 つの小道具を必要とします。このコンポーネントは、名を使用してパーソナライズされた挨拶を作成し、送信されたメッセージを表示します。

Resend APIを使用したメール送信の実装

こちら。 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 <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 });
  }
}
ログイン後にコピー

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 <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:

  • 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 (
      <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.

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

...

        <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

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

 ...     
        <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>
ログイン後にコピー
  • 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.

<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>
ログイン後にコピー
  • 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.

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

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<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:

  • 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<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:

  • 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<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 (
      

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" />