I know how tough it can be to build smooth, secure backend features, so I wanted to bring a little joy to your day. I made this polished gift redemption system as my little present to you! Inside it, you will find clean logic and a complete Node.js/MongoDB transaction.
The Logic:
1. Security & Validation
-
Authentication: The code checks if the user is logged in using the
checkAuthmiddleware. - ID Validation: It ensures the gift ID in the URL is a valid database ID.
2. Character Limit Math
- The maximum character limit allowed is 4000 characters.
- It calculates how close the user currently is to that limit.
- It attempts to grant the user a +100 character boost, capping it if they are already near the 4000-character.
-
Concurrency Handling: While the system may calculate the same
incvalue for concurrent requests, the atomicupdateOneoperation ensures that if applying both boosts exceeds the 4000-character limit, only the faster request succeeds.
3. Database Transaction (All or Nothing)
The system starts a database transaction. This guarantees that both the gift update and the user update must succeed together; otherwise, the entire operation rolls back.
- The query looks for an active gift that the current logged-in user has not claimed yet. It increments the gift's usage count (
usedCount), save the user's ID in theusedByarray so they cannot claim it again, and changes the status to "expired" if it just reached its max available uses. - Also, It adds the calculated character boost to the user's account.
If the gift is invalid, expired or already redeemed by the logged-in user, the code throws an error and aborts the entire transaction. Otherwise, it successfully returns a success: true response.
The Code:
app.post("/api/v1/redeem/gift-link/:id", checkAuth, [
param("id").exists().isMongoId()
], validateResult, async (req, res) => {
const id = req.cleanData.id;
const remaining = Math.max(0, 4000 - req.currentUser.maxPostContentCharsLength);
const inc = Math.min(100, remaining);
if (inc <= 0) return res.status(400).json({ error: "Gift redeem failed!" }); // Specify the exact error if wanted!
const session = await mongoose.startSession();
await session.withTransaction(async () => {
// Gift
const giftResult = await schemas.Gifts.updateOne(
{ _id: id, status: "active", usedBy: { $ne: req.session.userId } },
[
{
$set: {
status: {
$cond: {
if: { $eq: [{ $subtract: ["$usesCount", "$usedCount"] }, 1] },
then: "expired",
else: "$status"
}
},
usedCount: {
$cond: {
if: { $eq: ["$usedCount", "$usesCount"] },
then: "$usedCount",
else: { $add: ["$usedCount", 1] }
}
},
usedBy: {
$setUnion: [
{ $ifNull: ["$usedBy", []] },
[new mongoose.Types.ObjectId(req.session.userId)]
]
}
}
}
],
{ session }
);
if (giftResult.matchedCount === 0) throw new Error("GIFT_REDEEM_FAILED");
// User
const userResult = await schemas.Users.updateOne({
_id: req.session.userId,
maxPostContentCharsLength: { $lt: 4000 }
}, {
$inc: {
maxPostContentCharsLength: inc
}
}, { session });
if (userResult.matchedCount === 0) throw new Error("USER_UPDATE_FAILED");
});
await session.endSession();
return res.status(200).json({ success: true });
});
Note: A
try/catchblock is unnecessary here because I am using theexpress-async-errors.
If you want to view the entire system, check https://github.com/Hfs2024/Vlox.
Hope you liked your 🎁!
Top comments (3)
Who liked their 🎁 from me?
I love it. Thanks
You're welcome! 🎉