多くのオープンソースの請求書管理アプリが Laravel で構築されています。 Javascript 開発者として、React と Javascript に精通している開発者向けの「React ソリューション」を構築したいと考えていました。 Node.js でサービスを構築するときに私が見つけた問題は、メーラーが組み込まれていないことです。そのため、それを行うサードパーティのサービスを見つける必要がありました。この記事では、 を統合して、このプロジェクト のメールを送信します。 Courier https://github.com/fazzaamiarso/invoys 前提条件 この記事は一般的なフォロー アロングではないため (「じっと座って、私のやり方を見てください」というようなものです)、使用されているすべてのテクノロジに精通している必要はありません。ただし、Typescript と Next.js に精通していると、より迅速に理解できるようになります。 このブログのテクノロジー: : 型安全性とオートコンプリートが最適ですよね? Typescript : 初心者でもフルスタック アプリを構築するための本番環境対応フレームワーク。 Next.js : データベースを操作するための優れた ORM。型安全性とオートコンプリートのために Prisma を使用しており、typescript が追加された優れた開発者エクスペリエンスを提供します。 Prisma : Next.js クライアントとサーバーの間でエンドツーエンドの型安全性を簡単に構築できるようにします。 Trpc Courier API: メール、SMS などの通知を処理するための優れたサービス/プラットフォームです。 完全な あります。 ソース コードは、参照用にここに 目標 機能を構築する前に、目標を定義しましょう。 クライアントの電子メールに請求書のリンクを送信します。 請求書の期日の前日にリマインダーを送信します。 請求書がすでに支払われている場合、請求書の期日リマインダーをキャンセルします。 ネットワーク エラーの処理。 パート 1: Courier プラットフォームのセットアップ Courier ダッシュボードに行きましょう。デフォルトでは、本番環境にあります。テストしたいので、右上隅のドロップダウンをクリックしてテスト環境に変更します。 後ですべてのテンプレートを本番環境にコピーしたり、その逆にコピーしたりできます。 次に、メール通知用の を作成します。 ブランド ヘッダーにロゴを追加し (ロゴの幅は 140px に固定されていることに注意してください)、フッターにソーシャル リンクを追加します。デザイナーの UI は非常に単純なので、最終的な結果は次のとおりです。 変更を公開することを忘れないでください。 パート 2: 請求書を電子メールで送信する 現在、UI のメール送信ボタンは何もしていません。 Courier 関連のすべてのコードを保持するために に ファイルを作成します。また、すべての Courier API エンドポイントを関数に抽象化した を使用します。 src/lib/ courier.ts courier node.js クライアント ライブラリ 機能を構築する前に、Courier's Designer 内で電子メール通知デザインを作成し、Gmail プロバイダーをセットアップしましょう。 メール デザイナー ページでは、作成されたブランドが既に統合されていることがわかります。その後、必要なデータに応じてテンプレートを設計しましょう。これが最終結果です。 緑になる の値に注目してください。これは、動的に挿入できる変数であることを意味します。また、「請求書を見る」ボタン (またはアクション) に変数を設定しました。 {} テンプレートを使用する前に、プレビュー タブをクリックして を作成する必要があります。次に、イベントに名前を付け、JSON 形式で を設定するプロンプトが表示されます。そのデータ フィールドは、緑色の 変数の値を設定するものです (データはコードからも設定できます)。テストイベントなので、任意の値を入れます。 テスト イベント data {} 次に、使用できるようにテンプレートを公開します。次に、送信タブに移動します。電子メールをプログラムで送信するために必要なコードが表示され、以前に作成した に入力されます。 テスト イベントが data バックエンド テスト ファイルにコピーし、スニペットを にコピーします。 AUTH_TOKEN .env src/lib/courier.ts const authToken = process.env.COURIER_AUTH_TOKEN; // email to receive all sent notifications in DEVELOPMENT mode const testEmail = process.env.COURIER_TEST_EMAIL; const INVOICE_TEMPLATE_ID = <TEMPLATE_ID>; const courierClient = CourierClient({ authorizationToken: authToken, }); 電子メールの送信を担当する 関数を作成します。コードからメールを送信するには、 関数を使用します。 sendInvoice courierClient.send() // src/lib/courier.ts export const sendInvoice = async ({ customerName, invoiceNumber, invoiceViewUrl, emailTo, productName, dueDate, }: SendInvoice) => { const recipientEmail = process.env.NODE_ENV === "production" ? emailTo : testEmail; const { requestId } = await courierClient.send({ message: { to: { email: recipientEmail, }, template: INVOICE_TEMPLATE_ID, // Data for courier template designer data: { customerName, invoiceNumber, invoiceViewUrl, productName, dueDate, }, }, }); return requestId }; 関数のタイプを定義します。 sendInvoice // src/lib/courier.ts interface SendInvoice { productName: string; dueDate: string; customerName: string; invoiceNumber: string; invoiceViewUrl: string; emailTo: string; } 電子メールを送信できるようになったので、 にある trpc エンドポイントで呼び出します。 src/server/trpc/router/invoice.ts sendEmail を使用して 。 trpc エンドポイントは Next.js API ルートであることを覚えておいてください。この場合、 sendEmail 内部で fetch /api/trpc/sendEmail ルート を呼び出すのと同じです 詳細については、 参照してください。 https://trpc.io/docs/quickstart を // src/server/trpc/router/invoice.ts import { sendInvoice } from '@lib/courier'; import { dayjs } from '@lib/dayjs'; // .....SOMEWHERE BELOW sendEmail: protectedProcedure .input( z.object({ customerName: z.string(), invoiceNumber: z.string(), invoiceViewUrl: z.string(), emailTo: z.string(), invoiceId: z.string(), productName: z.string(), dueDate: z.date(), }) ) .mutation(async ({ input }) => { const invoiceData = { ...input, dueDate: dayjs(input.dueDate).format('D MMMM YYYY'), }; await sendInvoice(invoiceData); }), trpc に慣れていない人のために、私がやったことは リクエストの処理と同じです。分解してみましょう。 POST Zod で検証することにより、 を定義する Trpc の方法。ここでは、 関数に必要なすべてのデータを定義します。 クライアントからのリクエスト入力 sendInvoice .input( z.object({ customerName: z.string(), invoiceNumber: z.string(), invoiceViewUrl: z.string(), emailTo: z.string(), invoiceId: z.string(), productName: z.string(), dueDate: z.date(), }) ) リクエスト ハンドラーを定義します (ミューテーション)。 POST // input from before .mutation(async ({ input }) => { const invoiceData = { ...input, // format a date to string with a defined format. dueDate: dayjs(input.dueDate).format('D MMMM YYYY'), // ex.'2 January 2023' }; // send the email await sendInvoice(invoiceData); }), フロントエンド これで、メール送信ボタンに機能を追加できるようになりました。 useMutation` の薄いラッパーである 関数を使用します。 tanstack-query's trpc.useMutation() ミューテーション機能を追加しましょう。応答が成功したら、UI で成功のトーストを送信したいと思います。 //src/pages/invoices/[invoiceId]/index.tsx import toast from 'react-hot-toast'; const InvoiceDetail: NextPage = () => { // calling the `sendEmail` trpc endpoint with tanstack-query. const sendEmailMutation = trpc.invoice.sendEmail.useMutation({ onSuccess() { toast.success('Email sent!'); } }); } 関数をインライン ハンドラーとして使用できますが、ボタン用の新しいハンドラーを作成したいと考えています。 //src/pages/invoices/[invoiceId]/index.tsx // still inside the InvoiceDetail component const sendInvoiceEmail = () => { const hostUrl = window.location.origin; // prevent a user from spamming when the API call is not done. if (sendEmailMutation.isLoading) return; // send input data to `sendEmail` trpc endpoint sendEmailMutation.mutate({ customerName: invoiceDetail.customer.name, invoiceNumber: `#${invoiceDetail.invoiceNumber}`, invoiceViewUrl: `${hostUrl}/invoices/${invoiceDetail.id}/preview`, emailTo: invoiceDetail.customer.email, invoiceId: invoiceDetail.id, dueDate: invoiceDetail.dueDate, productName: invoiceDetail.name, }); }; これで、ハンドラーをメール送信ボタンにアタッチできます。 //src/pages/invoices/[invoiceId]/index.tsx <Button variant="primary" onClick={sendInvoiceEmail} isLoading={sendEmailMutation.isLoading}> Send to Email </Button> これが作業UIです。 パート 3: 支払い通知を送信する 請求書の期日の前日に送信されるリマインダーをスケジュールするには、 を使用します。 Courier の Automation API まず、Courier デザイナーでメール テンプレートをデザインしましょう。すでにプロセスを経ているので、これが最終的な結果です。 関数を追加する前に、パラメーターの型を定義し、型をリファクタリングします。 // src/lib/courier interface CourierBaseData { customerName: string; invoiceNumber: string; invoiceViewUrl: string; emailTo: string; } interface SendInvoice extends CourierBaseData { productName: string; dueDate: string; } interface ScheduleReminder extends CourierBaseData { scheduledDate: Date; invoiceId: string; } ここで、 に 関数を追加します。 src/lib/courier scheduleReminder //src/pages/invoices/[invoiceId]/index.tsx // check if the development environment is production const __IS_PROD__ = process.env.NODE_ENV === 'production'; const PAYMENT_REMINDER_TEMPLATE_ID = '<TEMPLATE_ID>'; export const scheduleReminder = async ({ scheduledDate, emailTo, invoiceViewUrl, invoiceId, customerName, invoiceNumber, }: ScheduleReminder) => { // delay until a day before due date in production, else 20 seconds after sent for development const delayUntilDate = __IS_PROD__ ? scheduledDate : new Date(Date.now() + SECOND_TO_MS * 20); const recipientEmail = __IS_PROD__ ? emailTo : testEmail; // define the automation steps programmatically const { runId } = await courierClient.automations.invokeAdHocAutomation({ automation: { steps: [ // 1. Set delay for the next steps until given date in ISO string { action: 'delay', until: delayUntilDate.toISOString() }, // 2. Send the email notification. Equivalent to `courierClient.send()` { action: 'send', message: { to: { email: recipientEmail }, template: PAYMENT_REMINDER_TEMPLATE_ID, data: { invoiceViewUrl, customerName, invoiceNumber, }, }, }, ], }, }); return runId; }; リマインダーを送信するには、 試行が成功した後に 呼び出します。 trpc エンドポイントを変更しましょう。 sendInvoice scheduleReminder sendEmail // src/server/trpc/router/invoice.ts sendEmail: protectedProcedure .input(..) // omitted for brevity .mutation(async ({ input }) => { // multiplier for converting day to milliseconds. const DAY_TO_MS = 1000 * 60 * 60 * 24; // get a day before the due date const scheduledDate = new Date(input.dueDate.getTime() - DAY_TO_MS * 1); const invoiceData = {..}; //omitted for brevity await sendInvoice(invoiceData); //after the invoice is sent, schedule the reminder await scheduleReminder({ ...invoiceData, scheduledDate, }); } ここで、電子メールで請求書を送信しようとすると、開発環境にいるため、20 秒後にリマインダーを受け取るはずです。 パート 4: リマインダーをキャンセルする 最後に、すべての機能の準備が整いました。しかし、問題が発生しました。クライアントが支払督促の予定日よりも前に支払った場合はどうなりますか?現在、リマインダー メールは引き続き送信されます。これは優れたユーザー エクスペリエンスではなく、クライアントが混乱する可能性があります。ありがたいことに、Courier には自動キャンセル機能があります。 に任意の自動化ワークフローをキャンセルできる 関数を追加しましょう。 src/lib/courier.ts cancelAutomationWorkflow export const cancelAutomationWorkflow = async ({ cancelation_token, }: { cancelation_token: string; }) => { const { runId } = await courierClient.automations.invokeAdHocAutomation({ automation: { // define a cancel action, that sends a cancelation_token steps: [{ action: 'cancel', cancelation_token }], }, }); return runId; }; cancelation_token とは何ですか?自動化ワークフローに設定できる一意のトークンであるため、対応する で アクションを送信することでキャンセルできます。 cancelation_token cancel に cancelation_token を追加し、請求書の Id をトークンとして使用します。 scheduleReminder // src/lib/courier.ts export const scheduleReminder = async (..) => { // ...omitted for brevity const { runId } = await courierClient.automations.invokeAdHocAutomation({ automation: { // add cancelation token here cancelation_token: `${invoiceId}-reminder`, steps: [ { action: 'delay', until: delayUntilDate.toISOString() }, // ... omitted for brevity trpc エンドポイントで請求書のステータスが に更新されたら、 を呼び出します。 updateStatus PAID cancelAutomationWorkflow // src/server/trpc/router/invoice.ts updateStatus: protectedProcedure .input(..) // omitted for brevity .mutation(async ({ ctx, input }) => { const { invoiceId, status } = input; // update an invoice's status in database const updatedInvoice = await ctx.prisma.invoice.update({ where: { id: invoiceId }, data: { status }, }); // cancel payment reminder automation workflow if the status is paid. if (updatedInvoice.status === 'PAID') { //call the cancel workflow to cancel the payment reminder for matching cancelation_token. await cancelAutomationWorkflow({ cancelation_token: `${invoiceId}-reminder`, }); } return updatedStatus; }), これが作業UIです。 パート 5: エラー処理 ネットワーク リクエストを行う際の重要な注意点は、失敗したリクエストやエラーが発生する可能性があることです。 UIに反映できるように、エラーをクライアントに投げて処理したい。 エラーが発生すると、Courier API はデフォルトで タイプのエラーをスローします。また、すべての関数の戻り値を に以下の形式と一致させます。 CourierHttpClientError src/lib/courier.ts // On Success type SuccessResponse = { data: any, error: null } // On Error type ErrorResponse = { data: any, error: string } これで、 内のすべての関数に ブロックを追加することで、エラーを処理できるようになりました。 src/lib/courier.ts try-catch try { // ..function code // modified return example return { data: runId, error: null }; } catch (error) { // make sure it's an error from Courier if (error instanceof CourierHttpClientError) { return { data: error.data, error: error.message }; } else { return { data: null, error: "Something went wrong!" }; } } trpc エンドポイントでの処理例を見てみましょう。 sendEmail // src/server/trpc/router/invoice.ts const { error: sendError } = await sendInvoice(..); if (sendError) throw new TRPCClientError(sendError); const { error: scheduleError } = await scheduleReminder(..); if (scheduleError) throw new TRPCClientError(scheduleError); パート 6: 本番環境への移行 すべてのテンプレートの準備が整ったので、テスト環境のすべてのアセットを本番環境にコピーします。ここに例があります。 結論 最後に、すべての機能が Courier に統合されています。 Courier API を Next.js アプリケーションに統合するワークフローを確認しました。 Next.js と trpc にありますが、ワークフローは他のテクノロジとほとんど同じです。 Courier を自分でアプリケーションに統合できるようになったことを願っています。 今すぐ始めましょう: https://app.courier.com/signup 著者について 私は、インドネシアのフルスタック Web 開発者である Fazza Razaq Amiarso です。私はオープンソースの愛好家でもあります。 の知識と学習を共有するのが大好きです。私は時々、空き時間に で他の開発者を支援しています。 ブログで自分 FrontendMentor で私とつながりましょう。 LinkedIn クイックリンク 🔗 クーリエドキュメント 🔗 インボイに貢献する 🔗 インボイのモチベーション にも掲載されています。 ここ