Facebook Pixel

Transactional Emails in Node.js Password Reset, Welcome, and Notification Emails

Learn how to implement common transactional emails in your Node.js app password reset, welcome emails, connection request notifications, and chat message alerts.

Transactional Emails in Node.js

Transactional emails are triggered by user actions signup, password reset, connection request, new message. This guide covers implementing the most common ones.

1. Welcome Email on Signup

// src/routes/auth.js const { sendEmail } = require("../utils/emailTemplates"); router.post("/signup", asyncHandler(async (req, res) => { const { firstName, lastName, email, password } = req.body; const user = new User({ firstName, lastName, email, password }); await user.save(); // Send welcome email (don't block the response) sendEmail(user.email, "welcome", { firstName: user.firstName }) .catch(err => console.error("Welcome email failed:", err)); // Generate JWT and send response const token = jwt.sign({ _id: user._id }, process.env.JWT_SECRET); res.cookie("token", token, { httpOnly: true, maxAge: 7 * 24 * 60 * 60 * 1000 }); res.status(201).json({ message: "User created", data: user }); }));

Key: Don't await the email sending the user shouldn't wait for the email to send before getting a response. Fire and forget, with error logging.

2. Password Reset Email

// src/routes/auth.js const crypto = require("crypto"); router.post("/password/forgot", asyncHandler(async (req, res) => { const { email } = req.body; const user = await User.findOne({ email }); if (!user) { // Don't reveal that the email doesn't exist return res.json({ message: "If the email exists, a reset link has been sent." }); } // Generate reset token const resetToken = crypto.randomBytes(32).toString("hex"); user.resetPasswordToken = crypto.createHash("sha256").update(resetToken).digest("hex"); user.resetPasswordExpires = Date.now() + 3600000; // 1 hour await user.save(); // Send reset email const resetLink = `https://app.yourdomain.com/reset-password?token=${resetToken}`; await sendEmail(user.email, "passwordReset", { resetLink, firstName: user.firstName }); res.json({ message: "If the email exists, a reset link has been sent." }); })); router.post("/password/reset", asyncHandler(async (req, res) => { const { token, newPassword } = req.body; const hashedToken = crypto.createHash("sha256").update(token).digest("hex"); const user = await User.findOne({ resetPasswordToken: hashedToken, resetPasswordExpires: { $gt: Date.now() } }); if (!user) { throw new AppError("Invalid or expired reset token", 400); } user.password = newPassword; user.resetPasswordToken = undefined; user.resetPasswordExpires = undefined; await user.save(); res.json({ message: "Password reset successfully" }); }));

3. Connection Request Notification

// src/routes/connection.js router.post("/request/send/:status/:toUserId", userAuth, asyncHandler(async (req, res) => { const { status, toUserId } = req.params; if (status !== "interested") { // Don't send email for "ignored" status return res.json({ message: "User ignored" }); } const fromUser = req.user; const toUser = await User.findById(toUserId); if (!toUser) throw new AppError("User not found", 404); const request = await ConnectionRequest.create({ fromUserId: fromUser._id, toUserId: toUser._id, status }); // Send notification email (async, don't block) sendEmail(toUser.email, "connectionRequest", { fromFirstName: fromUser.firstName, fromLastName: fromUser.lastName, toFirstName: toUser.firstName }).catch(err => console.error("Connection email failed:", err)); res.status(201).json({ message: "Connection request sent", data: request }); }) );

4. New Message Notification

// Socket.io event handler socket.on("message:send", async ({ targetUserId, text }) => { const message = await Message.create({ senderId: socket.user._id, receiverId: targetUserId, text }); // Check if receiver is online const receiverSocket = onlineUsers.get(targetUserId); if (!receiverSocket) { // Receiver is offline send email notification const receiver = await User.findById(targetUserId); const sender = socket.user; sendEmail(receiver.email, "newMessage", { senderFirstName: sender.firstName, message: text.substring(0, 100), chatLink: `https://app.yourdomain.com/chat/${sender._id}` }).catch(err => console.error("Message email failed:", err)); } io.to(roomId).emit("message:received", message); });

Email Template Examples

// Welcome email const welcome = (data) => ({ subject: "Welcome to DevTinder!", html: ` <h1>Hi ${data.firstName}!</h1> <p>Welcome to DevTinder the developer networking platform.</p> <p>Complete your profile to start connecting:</p> <a href="https://app.yourdomain.com/profile" style="background:#fd267d;color:white;padding:10px 20px;text-decoration:none;border-radius:5px;">Complete Profile</a> `, text: `Hi ${data.firstName}! Welcome to DevTinder. Complete your profile at https://app.yourdomain.com/profile` }); // Password reset const passwordReset = (data) => ({ subject: "Reset your DevTinder password", html: ` <h1>Password Reset</h1> <p>Hi ${data.firstName},</p> <p>Click the link below to reset your password. This link expires in 1 hour.</p> <a href="${data.resetLink}">Reset Password</a> <p>If you didn't request this, you can safely ignore this email.</p> `, text: `Reset your password: ${data.resetLink} (expires in 1 hour)` }); // Connection request const connectionRequest = (data) => ({ subject: `${data.fromFirstName} wants to connect!`, html: ` <h1>New Connection Request</h1> <p>Hi ${data.toFirstName},</p> <p><strong>${data.fromFirstName} ${data.fromLastName}</strong> wants to connect with you on DevTinder.</p> <a href="https://app.yourdomain.com/requests">View Request</a> `, text: `${data.fromFirstName} ${data.fromLastName} wants to connect. View at https://app.yourdomain.com/requests` });

Best Practices

  1. Don't block the response Send emails asynchronously (fire and forget)
  2. Handle failures gracefully Log errors but don't fail the request
  3. Use email queues for high volume Bull, BullMQ, or AWS SQS
  4. Don't send to bounced emails Track bounces and skip them
  5. Include unsubscribe links For notification emails (not transactional)
  6. Rate limit Don't send too many emails to one user
  7. Test in sandbox Verify before requesting production access

The Takeaway

Transactional emails are triggered by user actions: welcome on signup, password reset on forgot, connection request notifications, and new message alerts. Send emails asynchronously (don't block the response), handle failures gracefully, use templates for consistency, and track bounces to protect your sender reputation.

After creating the user, call sendEmail with the welcome template. Don't await the email use fire and forget (.catch for error logging) so the user gets their response immediately. The email sends in the background.

Generate a random token (crypto.randomBytes), hash it, save to user with 1-hour expiry. Send an email with a reset link containing the raw token. On reset, verify the hashed token and expiry, update the password, and clear the token.

No. Don't block the HTTP response on email sending. Use fire and forget (sendEmail().catch(err => console.error(err))) so the user gets their response immediately. The email sends in the background. For critical emails, use a queue (Bull, SQS).

In the Socket.io message:send handler, check if the receiver is online. If not, send an email notification with the sender's name, a snippet of the message, and a link to the chat. This keeps offline users informed without SMS/push notifications.

Send asynchronously (don't block responses), handle failures gracefully (log errors but don't fail the request), use email queues for high volume, don't send to bounced addresses, include unsubscribe links for notifications, rate limit per user, and test in sandbox mode first.

Ready to master Node.js completely?

Want to upskill yourself, crack your next interview, and get your dream job? Join our comprehensive course to dive deeper with high-quality video tutorials, solve interview questions, and a premium community.

Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.