Upstash Redis とワークフローを使用してスケーラブルなニュースレター アプリを作成する
このブログでは、ユーザーが購読してニュースレターを受け取る頻度を選択できるニュースレター アプリを構築します。 Upstash Redis を使用します。 サブスクリプション データとUpstash ワークフローを保存するため ユーザーの設定に基づいて、データの保存、ウェルカム メールの送信、ニュースレターのスケジュール設定などのアクションを管理します。
モチベーション
まず第一に、サーバーレス環境は素晴らしいです。拡張性が高く、予算も抑えられます。ただし、実行時間制限などの制限があります。これは、長時間実行されるタスクを実行する必要がある場合に特に問題となる可能性があります。
それがUpstash ワークフローです。 が登場します。 Upstash Workflow を使用すると、必要な限り実行できる永続的なワークフローを作成できます。したがって、サーバーレス関数のタイムアウトについて心配する必要はもうありません。
Upstash ワークフローを使用するときに得られる機能のリストは次のとおりです。
- サーバーレス関数のタイムアウトがなくなりました :ワークフローは必要な限り実行できます。
- 自動回復 :何か問題が発生してワークフローが途中で失敗した場合、自動的に回復します。
- 自動再試行 :ワークフロー内のいずれかのステップが失敗した場合、自動的に再試行されます。
- リアルタイム監視 :Upstash コンソールからワークフローをリアルタイムで監視できます。
前提条件
- Next.js アプリケーションの基本的な理解
- Redis および QStash トークン用の Upstash アカウント
- 導入用の Vercel アカウント。
- ローカル開発には ngrok (推奨)
プロジェクトのセットアップ
create-next-app を使用して新しい Next.js プロジェクトをブートストラップすることから始めましょう。 :
npx create-next-app@latest --typescript newsletter-app
cd newsletter-app ここで、Upstash QStash および Redis サービスと対話するために必要な依存関係を追加しましょう。
npm install @upstash/qstash @upstash/redis ディレクトリ構造
コードに入る前に、プロジェクトをどのように構成するかを簡単に見てみましょう。
src/app/:これは、メインのアプリケーション コンポーネントとページが存在する場所です。src/app/api/:ワークフローの登録、登録解除、処理のための API ルートをここに置きます。src/components/:このフォルダには、購読フォーム コンポーネントと購読解除フォーム コンポーネントが含まれます。src/lib/:Redis および電子メール送信用のユーティリティ関数がここに入ります。src/types/:TypeScript の型定義をこのディレクトリに保存します。
環境変数
.env を作成する必要があります。 ファイルをプロジェクトのルートに配置し、以下を追加します。
QSTASH_TOKEN=
UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN=
EMAIL_SERVICE_URL=
NEXT_PUBLIC_BASE_URL= - QSTASH_TOKEN :Upstash コンソールからアクセスされる Upstash QStash トークン。
- UPSTASH_REDIS_REST_URL と UPSTASH_REDIS_REST_TOKEN :Upstash コンソールからアクセスされる Upstash Redis 認証情報。
- EMAIL_SERVICE_URL :電子メール送信 API のエンドポイント。
- NEXT_PUBLIC_BASE_URL :デプロイされたアプリケーションのベース URL (例:
https://your-app.vercel.app) ).
UPSTASH_WORKFLOW_URL を設定することもできます。 .env の変数 ngrok URL を使用してローカル開発用のファイルを作成します。 ngrok を使用してローカルでワークフローを開発する方法の詳細については、Upstash ドキュメントを参照してください。
UPSTASH_WORKFLOW_URL 環境変数はローカル開発の場合にのみ必要です。運用環境では、baseUrl パラメータは自動的に設定されるため、省略できます。
プロジェクトの実施
サブスクリプション フォーム コンポーネント
SubscriptionForm このコンポーネントを使用すると、ユーザーは電子メールを入力し、ニュースレターを受信する頻度を選択できます。フォームが送信されると、POST リクエストが /api/subscribe に送信されます。 フォーム データを使用します。
"use client";
import React, { useState } from "react";
export default function SubscriptionForm() {
const [frequency, setFrequency] = useState("daily");
const [showCustomFrequency, setShowCustomFrequency] = useState(false);
const [message, setMessage] = useState("");
const [isError, setIsError] = useState(false);
// Handle frequency selection
const handleFrequencyChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const value = e.target.value;
setFrequency(value);
setShowCustomFrequency(value === "custom");
};
// Handle form submission
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setMessage("");
setIsError(false);
const formData = new FormData(e.currentTarget);
try {
const response = await fetch("/api/subscribe", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(Object.fromEntries(formData.entries())),
});
const result = await response.json();
if (!response.ok) {
setIsError(true);
setMessage(result.error || "An error occurred during subscription.");
} else {
setIsError(false);
setMessage(result.message || "Subscription successful!");
}
} catch (error) {
console.error("An unexpected error occurred:", error);
setIsError(true);
setMessage("An unexpected error occurred.");
}
};
// Render the form
return (
<form className="flex flex-col gap-4 text-gray-700" onSubmit={handleSubmit}>
<input
type="email"
name="email"
placeholder="Your Email"
required
className="border p-2 rounded"
/>
<select
name="frequency"
value={frequency}
onChange={handleFrequencyChange}
required
className="border p-2 rounded text-gray-700"
>
<option value="daily">Daily</option>
<option value="weekly">Weekly</option>
<option value="monthly">Monthly</option>
<option value="custom">Custom Amount of Days</option>
</select>
{showCustomFrequency && (
<input
type="number"
name="customFrequency"
placeholder="Enter number of days"
min="1"
className="border p-2 rounded text-gray-700"
required
/>
)}
<button type="submit" className="bg-blue-500 text-white p-2 rounded">
Subscribe
</button>
{message && (
<p className={`mt-2 ${isError ? "text-red-500" : "text-green-500"}`}>
{message}
</p>
)}
</form>
);
} 購読解除フォーム コンポーネント
UnsubscribeForm このコンポーネントを使用すると、ユーザーは電子メールを入力してニュースレターの購読を解除できます。フォームが送信されると、POST リクエストが /api/unsubscribe に送信されます。 メールデータと一緒に。また、ユーザーが電子メールのいずれかで購読解除リンクをクリックした場合、電子メール フィールドに事前に入力されます。
"use client";
import { useState, useEffect, Suspense } from "react";
import { useSearchParams } from "next/navigation";
const UnsubscribeForm = () => {
const searchParams = useSearchParams();
const [email, setEmail] = useState("");
const [message, setMessage] = useState("");
const [isError, setIsError] = useState(false);
// Pre-fill email from query parameter
useEffect(() => {
const emailParam = searchParams.get("email");
if (emailParam) {
setEmail(emailParam);
}
}, [searchParams]);
// Handle form submission
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setMessage("");
setIsError(false);
try {
const response = await fetch("/api/unsubscribe", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ email }),
});
const data = await response.json();
if (response.ok) {
setIsError(false);
setMessage("You have been unsubscribed successfully.");
} else {
setIsError(true);
setMessage(data.error || "Something went wrong. Please try again.");
}
} catch (error) {
console.error("Error unsubscribing:", error);
setIsError(true);
setMessage("An unexpected error occurred. Please try again.");
}
};
// Render the form
return (
<form className="flex flex-col gap-4 text-gray-700" onSubmit={handleSubmit}>
<input
type="email"
name="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Your Email"
required
className="border p-2 rounded"
/>
<button
type="submit"
className="bg-red-500 hover:bg-red-700 text-white p-2 rounded"
>
Unsubscribe
</button>
{message && (
<p className={`mt-2 ${isError ? "text-red-500" : "text-green-500"}`}>
{message}
</p>
)}
</form>
);
};
export default function UnsubscribePage() {
return (
<Suspense fallback={<div>Loading...</div>}>
<UnsubscribeForm />
</Suspense>
);
} Redis にデータを保存する
Upstash Redis を使用してユーザー サブスクリプション データを保存します。
Upsatsh Redis を使用するには、まず Upstash コンソールで Redis データベースをセットアップし、REST URL とトークンを取得する必要があります。これについて詳しくは、Upstash のドキュメントをご覧ください。
redis.ts これには、Redis クライアントと Redis と対話するためのヘルパー関数が含まれます。
import { Redis } from "@upstash/redis";
export const redis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL!,
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
});
export async function getUserFrequency(email: string): Promise<number | null> {
const data = await redis.get(`user:${email}`);
console.log("User data:", data);
if (!data) return null;
const parsed = JSON.parse(JSON.stringify(data));
return parsed.frequency;
}
export async function removeUser(email: string): Promise<void> {
await redis.del(`user:${email}`);
}
export async function checkSubscription(email: string): Promise<boolean> {
return (await getUserFrequency(email)) !== null;
} メール送信機能
電子メールを送信するには、QStash Python SDK を使用した電子メール スケジューラの作成に関する以前のブログ投稿で開発した独自の電子メール API を使用します。
src/lib/email.tsexport async function sendEmail(message: string, email: string) {
console.log(`Sending email to ${email}`);
const url = process.env.EMAIL_SERVICE_URL;
const payload = {
to_email: email,
subject: "Upstash Newsletter",
content: message,
};
if (!url) {
console.error("EMAIL_SERVICE_URL is not defined.");
return;
}
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
if (!response.ok) {
console.error("Failed to send email:", await response.text());
}
} 型の定義
サブスクリプション データの型定義も必要です。
src/types/index.tsexport type SubscriptionData = {
email: string;
frequency: string;
customFrequency?: string;
}; API ルートをサブスクライブ
サブスクリプションリクエストを処理する API ルートを作成します。ユーザーが購読フォームを送信すると、このエンドポイントはユーザーがすでに購読しているかどうかを確認し、ユーザーが選択した頻度に基づいて電子メールの送信を処理するワークフローをキューに入れます。
src/app/api/subscribe/route.tsimport { NextRequest, NextResponse } from "next/server";
import { checkSubscription } from "@/lib/redis";
export const POST = async (request: NextRequest) => {
try {
const { email, frequency: freq, customFrequency } = await request.json();
console.log("Email:", email);
console.log("Frequency:", freq);
console.log("Custom Frequency:", customFrequency);
if (!email || !freq) {
console.error("Email and frequency are required.");
return NextResponse.json(
{ error: "Email and frequency are required." },
{ status: 400 }
);
}
let frequency = freq;
if (frequency === "custom") {
if (!customFrequency) {
console.error("Custom frequency days are required.");
return NextResponse.json(
{ error: "Custom frequency days are required." },
{ status: 400 }
);
}
frequency = customFrequency;
}
if (frequency === "daily") {
frequency = "1";
} else if (frequency === "weekly") {
frequency = "7";
} else if (frequency === "monthly") {
frequency = "30";
}
const frequencyNumber = Number(frequency);
if (isNaN(frequencyNumber) || frequencyNumber <= 0) {
console.error("Invalid frequency value.");
return NextResponse.json(
{ error: "Invalid frequency value." },
{ status: 400 }
);
}
const exists = await checkSubscription(email);
if (exists) {
console.error("Email is already subscribed.");
return NextResponse.json(
{ error: "Email is already subscribed." },
{ status: 400 }
);
}
console.log("Subscription successful!");
console.log("Enqueue the workflow");
// Enqueue the workflow
await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/workflow`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.QSTASH_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
email: email,
frequency: frequencyNumber,
}),
})
.then((response) => {
if (!response.ok) {
console.error("Failed to enqueue workflow:", response.statusText);
return NextResponse.json(
{ error: "Failed to enqueue workflow." },
{ status: 500 }
);
} else {
console.log("Workflow enqueued successfully");
}
})
.catch((error) => {
console.error("Error enqueuing workflow:", error);
return NextResponse.json(
{ error: "Error enqueuing workflow." },
{ status: 500 }
);
});
return NextResponse.json({ message: "Subscription successful!" });
} catch (error) {
console.error("Error occurred:", error);
return NextResponse.json(
{ error: "An error occurred during subscription." },
{ status: 500 }
);
}
}; API ルートの登録を解除する
購読ルートがあるので、購読解除ルートも必要です。リクエストが行われると、ユーザーがサブスクライブされているかどうかを確認し、Redis からデータを削除します。確認メールも送信されます。
src/app/api/unsubscribe/route.tsimport { NextRequest, NextResponse } from "next/server";
import { redis } from "@/lib/redis";
import { sendEmail } from "@/lib/email";
export const POST = async (request: NextRequest) => {
try {
const { email } = await request.json();
if (!email) {
return NextResponse.json(
{ error: "Email is required." },
{ status: 400 }
);
}
const userExists = await redis.exists(`user:${email}`);
if (!userExists) {
return NextResponse.json(
{ error: "Email is not subscribed." },
{ status: 400 }
);
}
// Remove the user from Redis
await redis.del(`user:${email}`);
// Send an email to confirm unsubscription
await sendEmail(
"You have been unsubscribed from Upstash Newsletter.",
email
);
return NextResponse.json({ message: "You have been unsubscribed." });
} catch (error) {
console.error("Unsubscribe error:", error);
return NextResponse.json(
{ error: "An error occurred. Please try again." },
{ status: 500 }
);
}
}; ワークフロー API ルート
さて、ここからが楽しい部分です!指定された頻度でニュースレターを送信するためのワークフローを処理する API ルートを作成します。
私たちのワークフローは次のことを行います:
<オル>- 指定された頻度が続くまで待ちます。
- ユーザーがまだ購読しているかどうかを確認します。
- ニュースレターのメールを送信します。
- 無限ループは避けたいので、設定した数のニュースレターが送信されるまで繰り返します。
以下は、購読し、ニュースレターを 1 通受け取り、購読を解除したユーザーの完了したワークフローの例です。

Upstash コンソールからワークフローにアクセスして監視できます。
メイン ページ コンポーネント
アプリケーションのメインページを設定しましょう。このページには、購読フォームと購読解除ページへのリンクが含まれます。
src/app/page.tsximport SubscriptionForm from "@/components/SubscriptionForm";
import Link from "next/link";
export default function Home() {
return (
<main className="flex flex-col items-center justify-center min-h-screen p-4">
<h1 className="text-3xl font-bold mb-6">
Subscribe to Upstash Newsletter
</h1>
{/* Subscription Form */}
<SubscriptionForm />
{/* Unsubscribe Link */}
<div className="mt-8">
<p className="text-gray-600">
Already subscribed and want to unsubscribe?
<Link
href="/unsubscribe"
className="text-red-500 hover:text-red-700 font-bold ml-2"
>
Click here to unsubscribe
</Link>
</p>
</div>
</main>
);
} 購読解除ページ コンポーネント
最後に、購読解除ページを作成しましょう。
src/app/unsubscribe/page.tsximport UnsubscribePage from "@/components/UnsubscribeForm";
export default function UnsubscribeHome() {
return (
<main className="flex flex-col items-center justify-center min-h-screen p-4">
<h1 className="text-3xl font-bold mb-6">
Unsubscribe from Upstash Newsletter
</h1>
{/* Unsubscribe Form */}
<UnsubscribePage />
</main>
);
} 結論
そして、それができました!サーバーレス関数のタイムアウトを気にせずに、シンプルなニュースレター アプリを構築しました。
このプロジェクトの完全なソース コードは GitHub で見つけることができ、ライブ デモはここでチェックアウトできます。
Upstash ワークフローの詳細については、Upstash ドキュメントを参照してください。
ご質問がございましたら、Discord でお気軽にお問い合わせください。また、その他のチュートリアルや使用例については、Upstash ブログを参照することを忘れないでください。
-
Redis用のAzureキャッシュ、エンタープライズ層が一般提供になりました
今朝、MicrosoftとRedisは共同で、Redis、エンタープライズ層向けのAzureCacheの一般提供を発表しました。このサービスは昨年10月から公開プレビューされており、すでに本番Redisワークロードを顧客に提供しています。 GAリリースは、アクティブなジオレプリケーション(最大99.999%の可用性)のプレビューと、ますます多くのAzureリージョンに展開されている間、回復を伴うディスクの永続性によって強化されています。 エンタープライズ層:ハイライト 次のエンタープライズおよびフラッシュ層の機能が一般的に利用可能になりました: オープンソースのRedis6.0 :
-
LangChain、Faiss、Next.js を使用してカスタム AI チャットボットを構築する – 実践ガイド
この投稿では、Upstash、Next.js、LangChain、Fly.io を使用してオープンソースのカスタム コンテンツ AI チャットボットを構築した方法について説明します。 Upstash は、モデル トレーニングのスケジュールを立てるのに役立ち、寛大なレート制限と OpenAI API 応答のキャッシュの方法を提供してくれました。 使用するもの Next.js(フロントエンドとバックエンド) LangChain (言語モデルを利用したアプリケーション開発用フレームワーク) Upstash(QStash によるトレーニング モデルのスケジュール設定、レート制限と OpenA