এই ব্লগে, আমরা একটি নিউজলেটার অ্যাপ তৈরি করব যেখানে ব্যবহারকারীরা সাবস্ক্রাইব করতে এবং কত ঘন ঘন তাদের নিউজলেটার পেতে চান তা নির্বাচন করতে সক্ষম হবে। আমরা Upstash Redis ব্যবহার করব সাবস্ক্রিপশন ডেটা এবং Upstash ওয়ার্কফ্লো সংরক্ষণ করতে ব্যবহারকারীর পছন্দের উপর ভিত্তি করে ডেটা সংরক্ষণ, স্বাগত ইমেল পাঠানো, এবং নিউজলেটার নির্ধারণের কাজগুলি পরিচালনা করতে৷
অনুপ্রেরণা
প্রথমত, সার্ভারহীন পরিবেশ দুর্দান্ত! এগুলি বাজেটে অত্যন্ত মাপযোগ্য এবং সহজ। যাইহোক, তারা কিছু নির্দিষ্ট সীমাবদ্ধতার সাথে আসে, যেমন কার্যকর করার সময়সীমা। এটি বিশেষত সমস্যাযুক্ত হতে পারে যখন আপনার দীর্ঘমেয়াদী কাজগুলি চালানোর প্রয়োজন হয়৷
সেখানেই Upstash Workflow খেলার মধ্যে আসে আপস্ট্যাশ ওয়ার্কফ্লো দিয়ে, আপনি ক্রমাগত ওয়ার্কফ্লো তৈরি করতে পারেন যা যতক্ষণ প্রয়োজন ততক্ষণ চলতে পারে। সুতরাং, আপনাকে আর সার্ভারহীন ফাংশন টাইমআউট নিয়ে চিন্তা করতে হবে না।
আপস্ট্যাশ ওয়ার্কফ্লো ব্যবহার করার সময় আপনি যে বৈশিষ্ট্যগুলি পান তার একটি তালিকা এখানে রয়েছে:
- আর কোন সার্ভারহীন ফাংশন টাইমআউট নেই৷ :আপনার কর্মপ্রবাহ যতক্ষণ প্রয়োজন ততক্ষণ চলতে পারে৷
- স্বয়ংক্রিয় পুনরুদ্ধার :যদি কিছু ভুল হয়ে যায় এবং একটি ওয়ার্কফ্লো মাঝপথে ব্যর্থ হয়, এটি স্বয়ংক্রিয়ভাবে পুনরুদ্ধার হয়৷
- স্বয়ংক্রিয় পুনরায় চেষ্টা :কর্মপ্রবাহের কোনো পদক্ষেপ ব্যর্থ হলে, এটি স্বয়ংক্রিয়ভাবে পুনরায় চেষ্টা করা হবে।
- রিয়েল-টাইম মনিটরিং :আপনি আপস্ট্যাশ কনসোল থেকে রিয়েল-টাইমে আপনার কর্মপ্রবাহ নিরীক্ষণ করতে পারেন।
পূর্বশর্ত
- 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 QStash টোকেন Upstash Console থেকে অ্যাক্সেস করা হয়েছে।
- UPSTASH_REDIS_REST_URL এবং UPSTASH_REDIS_REST_TOKEN :আমাদের Upstash Redis শংসাপত্রগুলি Upstash কনসোল থেকে অ্যাক্সেস করা হয়েছে৷ ৷
- EMAIL_SERVICE_URL :আমাদের ইমেল পাঠানোর API এর শেষ পয়েন্ট।
- NEXT_PUBLIC_BASE_URL৷ :আমাদের স্থাপন করা অ্যাপ্লিকেশনের ভিত্তি URL (যেমন,
https://your-app.vercel.app)।
আমরা UPSTASH_WORKFLOW_URL সেট করতে পারি আমাদের .env এ পরিবর্তনশীল আমাদের ngrok URL সহ স্থানীয় উন্নয়নের জন্য ফাইল। এনগ্রোকের সাথে স্থানীয়ভাবে কর্মপ্রবাহ কীভাবে বিকাশ করা যায় সে সম্পর্কে আরও জানতে, আপস্ট্যাশ ডকুমেন্টেশন পড়ুন।
UPSTASH_WORKFLOW_URL পরিবেশ পরিবর্তনশীল শুধুমাত্র স্থানীয় উন্নয়নের জন্য প্রয়োজনীয়। উৎপাদনে, baseUrl প্যারামিটার স্বয়ংক্রিয়ভাবে সেট করা হয় এবং বাদ দেওয়া যেতে পারে।
প্রকল্প বাস্তবায়ন
সাবস্ক্রিপশন ফর্ম উপাদান
SubscriptionForm কম্পোনেন্ট ব্যবহারকারীদের তাদের ইমেল লিখতে এবং তারা কত ঘন ঘন নিউজলেটার পেতে চান তা নির্বাচন করতে দেয়। ফর্ম জমা দেওয়া হলে, আমরা /api/subscribe-এ একটি POST অনুরোধ পাঠাই ফর্ম ডেটা সহ।
"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 কম্পোনেন্ট ব্যবহারকারীদের নিউজলেটার থেকে সদস্যতা ত্যাগ করতে তাদের ইমেল প্রবেশ করতে দেয়। যখন ফর্মটি জমা দেওয়া হয়, আমরা /api/unsubscribe-এ একটি POST অনুরোধ পাঠাই৷ ইমেল ডেটা সহ। ব্যবহারকারী যদি কোনো একটি ইমেলে আনসাবস্ক্রাইব লিঙ্কে ক্লিক করেন তবে এটি ইমেল ক্ষেত্রটি পূর্বেই পূরণ করে।
"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;
} ইমেল পাঠানোর ফাংশন
ইমেল পাঠাতে, আমরা আমাদের নিজস্ব ইমেল API ব্যবহার করব যা আমরা QStash Python SDK এর সাথে একটি ইমেল শিডিউলার তৈরি করার বিষয়ে পূর্ববর্তী ব্লগ পোস্টে তৈরি করেছি৷
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 রুট তৈরি করব যা নির্দিষ্ট ফ্রিকোয়েন্সি ব্যবধানে নিউজলেটার পাঠানোর জন্য কর্মপ্রবাহ পরিচালনা করে।
আমাদের কর্মপ্রবাহ নিম্নলিখিত কাজ করবে:
- Redis-এ ব্যবহারকারীর সাবস্ক্রিপশন ডেটা সংরক্ষণ করুন।
- একটি স্বাগত ইমেল পাঠান৷ ৷
- একটি লুপ লিখুন:
- নির্দিষ্ট ফ্রিকোয়েন্সি সময়কালের জন্য অপেক্ষা করুন।
- ব্যবহারকারী এখনও সাবস্ক্রাইব করেছেন কিনা তা পরীক্ষা করুন।
- নিউজলেটার ইমেল পাঠান।
- পুনরাবৃত্তি করুন যতক্ষণ না নির্দিষ্ট সংখ্যক নিউজলেটার পাঠানো হয় যেহেতু আমরা একটি অসীম লুপ চাই না৷
এখানে একজন ব্যবহারকারীর জন্য একটি সম্পূর্ণ কর্মপ্রবাহের একটি উদাহরণ রয়েছে যিনি সদস্যতা নিয়েছেন, একটি একক নিউজলেটার পেয়েছেন এবং সদস্যতা ত্যাগ করেছেন:

আপনি 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 ডকুমেন্টেশন উল্লেখ করতে পারেন।
আপনার যদি কোন প্রশ্ন থাকে, বিনা দ্বিধায় আমাদের সাথে ডিসকর্ডে যোগাযোগ করুন। এছাড়াও, আরও টিউটোরিয়াল এবং কেস ব্যবহারের জন্য Upstash ব্লগটি অন্বেষণ করতে ভুলবেন না৷