
Join the Conversation!
Subscribing gives you access to the comments so you can share your ideas, ask questions, and connect with others.
"Please login to view comments"
Subscribing gives you access to the comments so you can share your ideas, ask questions, and connect with others.
If you’ve been paying attention to the architecture so far, you might have noticed that the User model already has a field for reputation. But we’re not showing it in the UI yet, are we?
Let’s start with that. Open app/(root)/profile/[id]/page.tsx and pass the reputation points to the Stats component. You can show it wherever you like, but since reputation is tied to badges and similar info, it makes sense to place it nearby. I will keep it there.
<Stats
totalQuestions={totalQuestions}
totalAnswers={totalAnswers}
badges={{
BRONZE: 0,
GOLD: 0,
SILVER: 0,
}}
reputationPoints={user.reputation || 0}
/>
Next, you’ll need to define the new variable you just passed to the component.
Head over to the Stats component and modify the type of Props passed to the component
interface Props {
totalQuestions: number;
totalAnswers: number;
badges: Badges;
reputationPoints: number;
}
Then include it in the Props restructure
const Stats = ({
totalQuestions,
totalAnswers,
badges,
reputationPoints,
}: Props) => {};
Finally, display the value near the h4 heading element
<h4 className="h3-semibold text-dark200_light900">
Stats{" "}
<span className="small-semibold primary-text-gradient">
{formatNumber(reputationPoints)}
</span>
</h4>;
By default, the reputation points for all users should be set to zero, so no worries there. To test it, you can head to your MongoDB Atlas database, update the reputation value for any user to a number of your choice, and see it in action
Feel free to expand this system however you like—more actions, different point values—it's totally up to you. Just know that the logic you’ll write next is based on the table above.
As mentioned in the last lesson, every user action should be saved as a record. It’s useful for tracking activity and later, for building recommendations.
Let’s set that up.
Open `` and define an enum for all valid user actions:
export const InteractionActionEnums = [
"view",
"upvote",
"downvote",
"bookmark",
"post",
"edit",
"delete",
"search",
] as const;
and then update your schema so the action field only accepts values from this list:
const InteractionSchema = new Schema<IInteraction>(
{
user: { type: Schema.Types.ObjectId, ref: "User", required: true },
action: {
type: String,
enum: InteractionActionEnums,
required: true,
},
actionId: { type: Schema.Types.ObjectId, required: true }, // 'questionId', 'answerId',
actionType: { type: String, enum: ["question", "answer"], required: true },
},
{ timestamps: true }
);
So nice. So far:
Next, it’s time to make the system actually work:
Create a new file lib/actions/interaction.action.ts where you’ll implement a function createInteraction.
But before jumping into the logic, you know the drill — define a validation schema and a type for the function’s input.
What do you think this function should take in? Keep in mind—it needs to be reusable and simple to work with. Take a moment and think it through
Ready to roll?
Let’s figure out what you need to create an interaction record. It’s pretty straightforward—just take a quick look at the interaction model.
You’ll need the following:
Now you might be thinking, “Wait, don’t we already have the user ID from our reusable action handler?”
Yes, and you’ll definitely use that authenticated user ID. But authorId here refers to the owner of the content the action is targeting. Remember our reputation logic?
We’re dealing with two types of users here:
Technically, we don’t need the target user just to save the interaction record. But we do need them when calculating reputation points.
Since interaction and reputation updates go hand-in-hand, it’s better to handle both in the same function—no need for two separate API calls.
Whoops, got a bit carried away with the backstory there. 😄
Anyway—now it’s time to actually define the validation schema for the createInteraction function inside the lib/validations.ts file.
export const CreateInteractionSchema = z.object({
action: z.enum(InteractionActionEnums),
actionTarget: z.enum(["question", "answer"]),
actionId: z.string().min(1),
authorId: z.string().min(1),
});
As always, don’t forget to define the type for these parameters in types/action.d.ts:
interface CreateInteractionParams {
action:
| "view"
| "upvote"
| "downvote"
| "bookmark"
| "post"
| "edit"
| "delete"
| "search";
actionId: string;
authorId: string;
actionTarget: "question" | "answer";
}
Now head over to lib/actions/interaction.action.ts and write the logic.
It’s just creating a record—something you’ve probably done a dozen times already. Who’s even counting at this point? 😄
import mongoose from "mongoose";
import { Interaction, User } from "@/database";
import { IInteractionDoc } from "@/database/interaction.model";
import action from "../handlers/action";
import handleError from "../handlers/error";
import { CreateInteractionSchema } from "../validations";
export async function createInteraction(
params: CreateInteractionParams
): Promise<ActionResponse<IInteractionDoc>> {
const validationResult = await action({
params,
schema: CreateInteractionSchema,
authorize: true,
});
if (validationResult instanceof Error) {
return handleError(validationResult) as ErrorResponse;
}
const {
action: actionType,
actionId,
actionTarget,
authorId, // target user who owns the content (question/answer)
} = validationResult.params!;
const userId = validationResult.session?.user?.id;
const session = await mongoose.startSession();
session.startTransaction();
try {
const [interaction] = await Interaction.create(
[
{
user: userId,
action: actionType,
actionId,
actionType: actionTarget,
},
],
{ session }
);
// Todo: Update reputation for both the performer and the content author
await session.commitTransaction();
return { success: true, data: JSON.parse(JSON.stringify(interaction)) };
} catch (error) {
await session.abortTransaction();
return handleError(error) as ErrorResponse;
} finally {
await session.endSession();
}
}
That part’s super simple—no overthinking needed.
You’re almost at the finish line. The final step in this lesson is assigning reputation points.
In the same file (lib/actions/interaction.action.ts), create a new function called updateReputation—or feel free to give it an even better name if inspiration strikes.
But before jumping into the code, pause for a sec and think, “What should this function take in? What’s its job?”
To update reputation properly, we’ll need:
That seems like all you really need. Based on the action and its type, you can figure out how many points should go to the performer and how many to the target user.
While you’re on this point, don’t forget to define a type for this new function updateReputation in types/action.d.ts
interface UpdateReputationParams {
interaction: IInteractionDoc;
session: mongoose.ClientSession;
performerId: string;
authorId: string;
}
All good.
Coming back to logic, which is simple but there’s a little twist: what if the performer and target are the same person?
Take the post action, for example. The user creating the post is also the content owner. That’s an edge case you’ll want to account for.
In every scenario, the function should assign points to both users. But if they happen to be the same person, make sure to apply the points just once—not twice.
Alright, enough planning—go ahead and call updateReputation function in lib/actions/interaction.action.ts right after creating a new interaction.
export async function createInteraction(
params: CreateInteractionParams
): Promise<ActionResponse<IInteractionDoc>> {
const validationResult = await action({
params,
schema: CreateInteractionSchema,
authorize: true,
});
if (validationResult instanceof Error) {
return handleError(validationResult) as ErrorResponse;
}
const {
action: actionType,
actionId,
actionTarget,
authorId, // person who owns the content (question/answer)
} = validationResult.params!;
const userId = validationResult.session?.user?.id;
const session = await mongoose.startSession();
session.startTransaction();
try {
const [interaction] = await Interaction.create(
[
{
user: userId,
action: actionType,
actionId,
actionType: actionTarget,
},
],
{ session }
);
// Update reputation for both the performer and the content author
await updateReputation({
interaction,
session,
performerId: userId!,
authorId,
});
await session.commitTransaction();
return { success: true, data: JSON.parse(JSON.stringify(interaction)) };
} catch (error) {
await session.abortTransaction();
return handleError(error) as ErrorResponse;
} finally {
await session.endSession();
}
}
Make sure to pass the session to the updateReputation function so that both steps—logging the interaction and updating the user's reputation—happen as a single atomic operation. I don’t need to spell this out for you, right? You’ve got this.
Now, go ahead and implement the reputation logic in the same file based on the action taken.
async function updateReputation(params: UpdateReputationParams) {
const { interaction, session, performerId, authorId } = params;
const { action, actionType } = interaction;
let performerPoints = 0;
let authorPoints = 0;
switch (action) {
case "upvote":
performerPoints = 2;
authorPoints = 10;
break;
case "downvote":
performerPoints = -1;
authorPoints = -2;
break;
case "post":
authorPoints = actionType === "question" ? 5 : 10;
break;
case "delete":
authorPoints = actionType === "question" ? -5 : -10;
break;
}
if (performerId === authorId) {
await User.findByIdAndUpdate(
performerId,
{ $inc: { reputation: authorPoints } },
{ session }
);
return;
}
await User.bulkWrite(
[
{
updateOne: {
filter: { _id: performerId },
update: { $inc: { reputation: performerPoints } },
},
},
{
updateOne: {
filter: { _id: authorId },
update: { $inc: { reputation: authorPoints } },
},
},
],
{ session }
);
}
This probably didn’t go quite how you pictured it, did it?
But hey—that’s programming. There’s always more than one way to get something done.
Whether you used a promise or ran your database operations one after the other, it can still work. But each approach comes with its own tradeoffs.
Sequential Database Calls
The most straightforward approach is to perform your DB operations one after another, like so:
async function updateReputationSequential(params: UpdateReputationParams) {
if (performerId === authorId) {
await User.findByIdAndUpdate(performerId, { $inc: { reputation: authorPoints } }, { session });
return;
}
await User.findByIdAndUpdate(performerId, { $inc: { reputation: performerPoints } }, { session });
await User.findByIdAndUpdate(authorId, { $inc: { reputation: authorPoints } }, { session });
}
Sure, you can wrap it in a transaction with a session to keep it atomic, but it’s still sequential.
That means MongoDB processes one request at a time, which increases latency and could create performance bottlenecks under high load.
Parallel Database Calls
A slightly better approach is to fire both DB operations in parallel:
async function updateReputationParallel(params: UpdateReputationParams) {
if (performerId === authorId) {
await User.findByIdAndUpdate(performerId, { $inc: { reputation: authorPoints } }, { session });
return;
}
await Promise.all([
User.findByIdAndUpdate(performerId, { $inc: { reputation: performerPoints } }, { session }),
User.findByIdAndUpdate(authorId, { $inc: { reputation: authorPoints } }, { session }),
]);
}
This speeds things up, but if you’re not using transactions, there’s still a risk: one update might succeed, and the other might fail—leading to inconsistent data. And you’re still making multiple requests to the database.
Bulk Write Operations (Recommended)
The most efficient (and my preferred) method is using a bulk write
await User.bulkWrite([
{
updateOne: {
filter: { _id: performerId },
update: { $inc: { reputation: performerPoints } },
...(session && { session }),
},
},
{
updateOne: {
filter: { _id: authorId },
update: { $inc: { reputation: authorPoints } },
...(session && { session }),
},
},
]);
This might not be as clean to look at, but under the hood, it’s a single round-trip to the database. It plays very well with transactions and is more scalable when things get busy.
There are even more advanced approaches, like using $cond in aggregation pipelines, but that can get quite complex. If you’re curious, check out MongoDB's $cond operator.
There is no single “correct” approach. Each approach has pros and cons, so choose the one that best suits your performance and consistency needs.
I went with bulkWrite here so we can handle multiple DB operations in one go—rather than firing off separate requests. It’s faster, cleaner, safer, and scales much better.
That was a lot to cover—but you pulled through.
Onto the next one 🚀
Complete source code for this lesson is available at
How did you manage to remove the blur property and reach here?
Upgrading gives you access to quizzes so you can test your knowledge, track progress, and improve your skills.
00:00:00 But that's not where it stops.
00:00:02 It's time to build a logic that actually changes a user's reputation based on what they do.
00:00:09 These actions can be specific to what you, the developer, want.
00:00:14 Do you want to track what kind of content the user is posting?
00:00:17 Or are you interested in upvotes or downvotes they're voting?
00:00:22 Or do you simply want to know what kind of content they're viewing?
00:00:25 You can track whatever you want.
00:00:27 But to keep things consistent, here's a naming pattern I would like to use for different actions.
00:00:34 We'll track an action when a user creates a question or an answer, and we'll call that action a post action.
00:00:42 Then we'll track different upvotes, downvotes, deletions, bookmarks, and searches.
00:00:50 You get the idea.
00:00:51 The list can go on and on, but you've got the pattern.
00:00:54 Now, the goal is to assign some of these reputation points to these actions.
00:01:00 You can totally come up with your own system, but here's a sample that I found works very well.
00:01:05 So now we take that same previous post action that we had before and we give the performer or the user who does the action a specific number of points.
00:01:14 For example, 5 for the question, 10 for the answer.
00:01:18 And then we can also give the target user, the user who owns the question or the answer, the same number of points.
00:01:25 Similar thing for the upvotes, for example, we can give the user who gives upvotes, maybe two points, and then a user who receives an upvote,
00:01:33 like five or ten points.
00:01:35 We can deduct points for downvotes, same thing for deletions, and maybe we also have a bookmark action.
00:01:42 Who knows?
00:01:43 Feel free to expand this system however you'd like with more actions, different point values.
00:01:49 It is totally up to you.
00:01:51 Just know that what we'll do in this application is based on this table that you're seeing right here.
00:01:58 5 for the question, 10 for the answer, 2 for the upload, 10 if you receive an upload, minus 1 or minus 2 for downvotes, minus 5, minus 10 for deletions.
00:02:09 Let's make it happen.
00:00:00 Okay, we've laid out the groundwork for creating our interaction.
00:00:05 But now, based on different interactions, we have to update the reputation.
00:00:11 So within the interaction.action.ts file, let's go ahead and create our second server action of the day.
00:00:18 I'll say async function update reputation.
00:00:23 And this one will accept different params of a type update reputation params.
00:00:29 These ones are coming from action.d.ts where we're defining the type of the interaction, the session, as well as the performer ID and then the author ID.
00:00:39 So we know who performed the action or the interaction and to whose question or answer.
00:00:46 Once we have that, we can extract all of those important things from params by saying const.
00:00:52 destructure the interaction, the session, the performer ID, and the author ID from params.
00:01:00 Then we also need to destructure the action and the action type from the interaction.
00:01:05 Based on that action, the action type, we can then give a specific number of points to each action.
00:01:12 So let's first set the default number of points.
00:01:16 Performer points or performer reputation, I'll set it equal to zero.
00:01:22 And I'll do the same thing for the author points.
00:01:25 Think of this as the starting point.
00:01:28 Then I'll open up a switch case that'll take a look at the action that a specific person is doing.
00:01:34 And then based on the action, so different cases, like an upvote maybe, we can give a specific number of points to the performer and to the author.
00:01:44 So if somebody upvotes, they'll only get about two points.
00:01:49 Not a big deal because we don't want them to just spam like everywhere.
00:01:53 But the author that received that upvote will receive 10 points because it's a big deal to receive a like from somebody on the internet.
00:02:00 We can do a similar thing for the downvotes.
00:02:03 So I'll say case, downvote.
00:02:05 And in this case, we can do a similar thing, but we'll deduct a specific number of points.
00:02:11 From the performer, we'll deduct one because they're being nasty on the internet and disliking other people's posts.
00:02:17 And from the author, we'll take away two points.
00:02:21 Of course, feel free to change this.
00:02:22 That's totally okay.
00:02:24 Now the most important thing is when we post something.
00:02:28 So we have a case of post and here we want to play with the author points.
00:02:32 So I'll say author points is equal to action type.
00:02:37 So if action type is a question, then we want to give it five points.
00:02:43 Else we want to give it 10 points.
00:02:45 And I'll also add a break.
00:02:47 And in case of a delete, so if we're deleting something, in that case, we want to do something similar as to here, author points,
00:02:57 action type is question, minus five and minus 10. And then we break it.
00:03:02 We're doing this according to the table that you have seen when we're discussing the reputation system.
00:03:07 Just below this, we can add an if statement and check if the performer ID Is triple equal to the author ID?
00:03:15 Well, that must mean that something is going wrong.
00:03:17 They should not be able to increase or decrease their own reputation, right?
00:03:21 But the question is, where are these performer IDs and author IDs coming from?
00:03:27 Well, they're coming from right here at the top.
00:03:30 Performer ID and the author ID from perhaps.
00:03:33 Let's just make sure to spell it properly.
00:03:35 If that is the case, we want to await.
00:03:39 user.findById and update.
00:03:43 We want to get access to what we want to update, which is the performer ID.
00:03:48 And we want to increase the reputation by a specific number of author points.
00:03:54 like this.
00:03:55 And then once we increase that reputation, we simply want to say return, exit out of the function.
00:04:00 Finally, at the end, we want to say await user.bulkWrite.
00:04:06 I think this is the first time that we're using this function.
00:04:09 And here you can have multiple operations, such as a first object where you want to update one, where the filter is set to underscore ID is performer ID.
00:04:21 And you also want to update, increase the reputation point for the performance point right here.
00:04:28 And then in the second operation, you want to also update one, this time the author, and you want to increase the reputation by the number of author points.
00:04:39 Make sense?
00:04:41 And finally, you can also just say that this belongs to a specific session.
00:04:45 So if something with the operation goes wrong, you don't update anything.
00:04:50 Perfect.
00:04:51 And now this update reputation.
00:04:54 is being used within this function we created before for both performer and the content author.
00:05:00 It's being used within this create interaction function.
00:05:04 So very soon we'll be able to test this update reputation out with all sorts of different interactions based on the table we have created before giving
00:05:14 or deducting a specific number of points.
00:05:16 So with that in mind, let's go ahead and commit it.
00:05:19 So I'll say something like, develop the update reputation logic, commit and sync, and in the next lesson, we'll be able to test it out.