
No Comments Yet
Be the first to share your thoughts and start the conversation.
Be the first to share your thoughts and start the conversation.
Complete source code available till this point of lesson is available at
By logging in, you'll unlock full access to this and other free tutorials on JSM Pro.
Why? Logging in lets us personalize your learning experience, track your progress, and keep you in the loop with new workshops, coding tips, and platform updates.
You'll also be the first to know about upcoming launches, events, and exclusive discounts.
No spam—just helpful content to level up your skills.
If that sounds fair, go ahead and log in to continue →
Enter your name and email to get instant access
In this lesson, we explore how to fetch and display questions on the homepage, utilizing various criteria for sorting and filtering data. Additionally, we implement pagination and discuss the structure and validation of search parameters.
paginatedSearchParams
for handling pagination parameters, including optional fields for page
, pageSize
, query
, filter
, and sort
.populate
to fetch related data for tags and authors efficiently.00:00:02 Now that we've created different questions, what do you say that we show them on the homepage?
00:00:07 You'll see that there are many different elements, and alongside just the questions, you can see that there are many other elements that determine which
00:00:16 questions we want to fetch on that page.
00:00:17 Criteria like searched questions, filters or sorts, or just fetch all the questions if none of the criteria is selected.
00:00:25 And let's not forget, there also has to be a pagination right here at the bottom.
00:00:29 And to achieve all of this data fetching through one common function with pagination, we'll use different Mongoose methods for sorting and filtering.
00:00:38 So let's head over into global.d.ts to create the types for that homepage.
00:00:45 Right here, we can say interface, and let's call it paginated search params.
00:00:53 because even though we're working on the home page, we can reuse this search anywhere where we have the pagination and the search of the elements.
00:01:02 So we'll have an optional page parameter of a type number.
00:01:05 We'll have a page size parameter of a type number as well, so we know how many elements on the page we want to display.
00:01:13 We'll also have a query element right there, so we can search for some posts based on the query.
00:01:20 In the same way, we can have a filter, which is going to be optional of a type string.
00:01:26 And finally, we can have a sort optional of a type string.
00:01:30 Next, as you already know, we also have to add these to the validation.
00:01:35 So right here, I can say export const paginated search params schema.
00:01:44 is equal to z.object.
00:01:48 And then we can pass all of these fields, such as a page equal to z.number.integer.positive.
00:01:57 And instead of making it optional, let's actually make it a default of 1. When it comes to the page size, it'll also be a positive integer.
00:02:06 And let's also not make it optional, but rather make it default to 10. It's pretty cool to provide a default right here,
00:02:14 that way you don't have to do it in every single other page or use that page size and don't inherently declare it.
00:02:20 Query will be an optional string, so will the filter and the sort.
00:02:24 Finally, we can head over to our question.action.ts and alongside create, edit, and get single question, we'll also create a new server action which will
00:02:36 fetch all the questions based on the search criteria.
00:02:40 So let's say export async function, get questions, questions plural, where we get the params of the name of paginated search params.
00:02:52 The return of this function will once again be a promise of an action response like this.
00:03:01 and to this action respond.
00:03:03 And specifically, it'll be an action response where we get back the access to the questions, which will be of a type question array,
00:03:11 as well as is next, to know whether the next page exists so we can properly paginate to it.
00:03:18 Let's properly close this and let's open up the function block.
00:03:21 First things first, we want to validate the results of our params.
00:03:24 By saying const validation result, is equal to await action to which we can pass the params as well as the schema equal to paginated search params schema.
00:03:37 So we know based off of which schema we're validating the data.
00:03:40 As usual, if the validation result is an instance of error, then we simply return handle error, validation error as error response.
00:03:50 And if everything is good, we can destructure some things from the params.
00:03:55 You can get it either directly from Params or from validationresult.params, doesn't really matter.
00:04:02 In this case, we're getting the page and we can default it to one right here as well.
00:04:07 We're also getting the page size, which we can default to 10. We're getting the query and we're getting the filter.
00:04:14 By the way, if you're not sure about this syntax, where you destructure some things and then you put an equal sign, this is different from putting something
00:04:22 like a colon and then discussing page number or something like that, where you're essentially renaming this variable.
00:04:30 If you provide an equal sign, if a value of this variable doesn't exist, it'll default to 1. And we can also define something known as a skip value,
00:04:39 which will calculate the number of documents to skip based on the current page.
00:04:43 For example, for page 2 and page size of 10, we want to skip 10 elements because they belong to the first page.
00:04:51 So we can say something like, let's convert our page into a number, minus 1, okay?
00:04:58 That's because page 2 will be 2 minus 1 to skip the first, how many?
00:05:04 Well, however many elements is on a page.
00:05:09 And that's going to look something like this.
00:05:11 So once again, if we're on page 3, that is 3 minus 1 is 2 times 10. So that's skip 20 elements and show me the next 10. We can also set the limit to be
00:05:22 equal to the page size.
00:05:23 And then we can start defining the filters.
00:05:26 By first creating a new filter query variable, which will be of a type filter query coming from Mongoose, and it'll be a type of question.
00:05:38 So this is what we're filtering.
00:05:40 And at the start, it'll be completely empty, just an empty object.
00:05:44 But only then, if some filters exist, we want to add it to that filter query object.
00:05:49 By saying if filter is triple equal to recommended, In that case, we want to return an object saying success is true, data will be equal to questions,
00:06:06 empty array, and isNext will be set to false.
00:06:10 Now, what are we really doing here?
00:06:12 Well, for now, we'll completely skip the selection of the recommended filter because we'll develop this logic later when we start tracking user behavior
00:06:21 on the platform.
00:06:22 For now, just skipping it is the way to go.
00:06:25 Next, let's check if we have access to a query.
00:06:28 And if we do, we can say filter query dot dollar sign or.
00:06:35 is equal to an array where we either search for a title or search for the content based on the case insensitive version of that text,
00:06:46 which we do through a regular expression.
00:06:48 So basically, if you search for something like test, it'll also match test like this or test like this.
00:06:56 We don't care about the capitalization.
00:06:58 Perfect.
00:06:59 So we're applying a query.
00:07:02 And the last thing we have to do is also apply sort criteria.
00:07:06 So I'll say let sort criteria is equal to an empty object.
00:07:12 And I'm purposely putting it as a let because I want to modify it later on by adding a switch statement, which is going to get access to the filter.
00:07:23 And if the case is equal to newest.
00:07:27 In that case, we can change the sort criteria to be equal to created at minus one, because we want to sort from the newest to the oldest.
00:07:38 And in this application, we'll be using the term filter and sort interchangeably, but you'll see in many other applications,
00:07:45 they're actually divided.
00:07:46 Filter is taking the data you have and presenting only some of the pieces, whereas sorting presents all of the pieces of data,
00:07:53 but then sorts them in a specific order.
00:07:56 But you'll see in our case why this makes sense.
00:07:58 We can also have another case which is going to be unanswered.
00:08:02 In this one, we'll actually apply a filter.
00:08:05 So it'll be equal to sort criteria is equal to created at minus one.
00:08:12 So same as before.
00:08:13 But before applying this, we'll apply a filter query that answers is equal to zero.
00:08:21 So we want to only have the questions that have zero answers.
00:08:26 Let's also have another, which is going to be a case of popular.
00:08:30 And here we can sort based on the criteria of upvotes.
00:08:36 So the more upvotes we have, the higher they will show on our sorting.
00:08:41 And finally, we can have a default case where we can just have a sort criteria created at minus one, which will show the newest ones at the top.
00:08:50 There we go.
00:08:51 Let me fix this typo right here.
00:08:53 and we are good.
00:08:54 And now you can see how easy it is to add additional filters.
00:08:57 You just add a case for it and then you modify the way that you fetch the data from the database based on that search or filter.
00:09:05 Finally, once you add all the filters or sorts you want to have, we can open up a new try and catch block.
00:09:12 In the catch, we can simply return handleError as errorResponse.
00:09:17 And in the try, we will try to fetch the questions by saying const questions is equal to await question.find based on the filter query that we have constructed.
00:09:32 We have not even created it or declared it.
00:09:35 In this case, I would use the word constructed because we have first created an empty filter query and then based on different criteria that we have,
00:09:45 we have constructed the final query which we can now simply pass to the questions.find.
00:09:51 And based on that, we can apply all searches, sorts, filters, and pagination all at once.
00:09:57 Then we can use a couple of different methods on this question that find such as .populate.
00:10:03 We want to populate the tags specifically with the name of the tag.
00:10:08 Then we want to run another populate and we want to populate the author field with the name and the image of the author for each one of these questions.
00:10:17 We want to make it lean.
00:10:18 And lean means that it'll convert this MongoDB document into a plain JavaScript object that makes it easier to work with.
00:10:26 And when it comes to populate, if you pass over a second parameter to it, it'll only make sure to return these specific fields of that first specific collection.
00:10:35 So this will only return tag names and this one will only populate author name and image.
00:10:41 And if you think about it, we don't need anything else.
00:10:43 We only need to get the image and the name of the author.
00:10:46 And for the tags, we just have to get their names.
00:10:49 Nothing else, not to make our application too bulky or too slow.
00:10:54 We only want to fetch what we need.
00:10:56 Finally, we can sort based on the sort criteria, similar to filter query.
00:11:01 And then we can skip a specific amount to do the pagination.
00:11:05 And finally, we can limit how many of the elements appear per page based on the limit property.
00:11:11 Of course, it's easy once I showed you how to do it, but on your own, crafting something like this for the first time, well,
00:11:17 it might be a bit complicated.
00:11:19 But now that you can see how abstract this is and how you can use it for any kind of documents and not just stack overflow questions,
00:11:27 I think your brain will immediately start making connections to the different places where you could use this exact pieces of code.
00:11:34 Once we finish this lesson, you can find this commit in the repo, bookmark it somewhere, and whenever you need to implement search sorting or filtering,
00:11:42 you can refer to this function.
00:11:43 Next, we have to figure out whether a next page exists.
00:11:47 And we can do that by saying isNext is equal to, and now we have to figure out how many questions there are.
00:11:53 And it's very easy to do that by using a new Mongoose method.
00:11:58 const totalQuestions is equal to await question.countDocuments to which we can pass the filter query so it knows what we want to filter out.
00:12:09 Once we have it, we can say totalQuestions is greater than skip plus questions.length.
00:12:17 So now if we're on the first page, seeing the first 10 elements, and if the total is about 15, that means that the total questions will be greater than
00:12:25 the number of the questions we have seen so far, plus the remaining questions, and then we have the next page.
00:12:31 So let's return an object where we say success is true.
00:12:36 Questions is set to json.parse json.stringify, and to it, we can pass a list of questions.
00:12:46 And finally, we pass the isNext variable, so we know whether our next page exists.
00:12:50 And once again, the reason why we're running this json.parse and json.stringify is to ensure compatibility with Next.js server actions,
00:12:59 because when you try to pass large payloads through server actions, sometimes it doesn't pass them properly and you get an error,
00:13:05 but this fixes that error.
00:13:07 And now we can show the results of this server action on the UI side.
00:13:12 So let's head over to root page.tsx.
00:13:16 Let's remove the dummy array of questions right here.
00:13:19 Finally, right?
00:13:20 And let's call the get questions to display the content.
00:13:24 We can do that by saying const.
00:13:27 We can destructure the page, the page size, the query, and filter, which are going to come from search params.
00:13:34 Oh, I noticed that we already did that at the top, but now we have two more.
00:13:39 So I will remove the previous search params destructuring.
00:13:43 And now we can get the questions by saying const destructure the success, the data, and the error.
00:13:50 Make it equal to an await call to our getQuestionsServerAction.
00:13:56 Make sure that it is getQuestions, not question, plural.
00:14:00 And to it, we need to pass a couple of things.
00:14:03 We have to pass a page equal to a number of a page, or default it to one if we don't have it.
00:14:10 And if you can, please try to answer this question.
00:14:13 Why are we wrapping the page into a number?
00:14:17 Well, that's because we have URL search programs.
00:14:21 Like you have some kind of a URL.com and then you have different programs like pages one, page sizes 10 and so on.
00:14:28 The thing is, whatever you pass here, be that maybe a true keyword or be that a number, it is just a string of URL.
00:14:37 So at the end of the day, it turns into a string and you end up with number one as a string or maybe true as a string.
00:14:44 So what you have to do is you have to convert it into a number, boolean, or whatever else you want to use.
00:14:50 Let's do a similar thing with the page size, the query, and let's do the same thing for the filter.
00:14:57 Finally, we can destructure the questions out of the data by saying const questions is equal to data or an empty object if data doesn't exist.
00:15:09 Finally, I believe we no longer have to do any kind of filtering right here on the front end.
00:15:13 So I will actually comment out for the time being these filtered questions.
00:15:18 And I'll head down below the home filters, where we have this div with margin top 10. And I'll simply check whether questions exist.
00:15:29 And if they do, I'll say questions.length.
00:15:32 is greater than zero and if that is true only then do we run questions.map but else we exit out of this if we have no questions we can return a div and
00:15:46 properly close it this div will have a class name of margin top of 10 flex w-full items-center and justify-center.
00:15:59 And within it, we can have a simple p tag with a class name equal to text-dark400, light 700. And we can say no questions found.
00:16:14 But if we do have questions, we'll say questions.map where we get each individual question and we render a question card for each one of the questions.
00:16:23 I also want to do another thing right here, and that is wrap this outer div in another check and see only if we have a success,
00:16:33 then we want to render that div right here.
00:16:35 So I'll pull it right here above.
00:16:38 Else, if we don't have a success, if something went wrong, we can render another div.
00:16:45 This div will have a class name equal to margin top of 10, flex, w-full, items-center, and justify-center.
00:16:57 And within it, we can render another p tag, very similar to this one.
00:17:03 But this one won't say no questions found.
00:17:06 Rather, it'll either display the error dot message if it has one.
00:17:11 Make sure to put the question mark there so the app doesn't break if it doesn't have access to the error object.
00:17:17 or we can display a failed to fetch questions error message.
00:17:26 Great.
00:17:26 So now we're checking for success and then we're checking whether we have any questions or whether we don't and we're displaying it in a proper way.
00:17:34 So let's go back to the browser only to be able to see Schema hasn't been registered for model user, use mongoose.model name and schema.
00:17:44 Now, this is happening when you try to reference a model, in this case, the user model, that hasn't been properly registered in mongoose or when there
00:17:54 is a mismatch between how the model is defined and how it's being used.
00:17:58 In our case, since we're already logged in and we haven't interacted with the user model yet, it throws this issue as we try to populate the author in
00:18:07 the getQuestions action.
00:18:09 If we head back to this question action, head over to getQuestions and then comment down the population of the author's name and image.
00:18:19 If you log out and then log back in, You'll see a different error, which is often a good thing.
00:18:25 And this one is happening on line 20 of the question card.
00:18:28 So let's head over to question card, line 20, specifically within the get timestamp, where we're passing the created at property.
00:18:37 If you check this out, you can see that we are referring to the date coming right here.
00:18:41 Well, what we are actually passing is the created at time.
00:18:45 So we can rename this to created at, and then we can define the date manually by saying const.
00:18:51 date is equal to new date out of the created ad timestamp.
00:18:56 If you do this, we should be good.
00:18:58 And now we do have some errors, which we'll check out very soon.
00:19:02 But as you can see, we're actually getting back the questions that we asked.
00:19:06 And the reason why we were not able to see the user data before is because we have to call our user model somewhere in the code to be able to see the content.
00:19:15 This happens due to the serverless nature of our code.
00:19:18 only the needed things are loaded.
00:19:21 Nothing else.
00:19:22 But that's not ideal, is it?
00:19:24 We need to ensure that all models are already loaded before we run any logic, and there are many ways, from preloading to lazy loading and more.
00:19:32 In our case, we'll preload all the models while we're making a database connection.
00:19:37 We can fix it by creating a new file within database, and then create a new file called index.ts.
00:19:45 Within this file, we want to import and export all of the other models.
00:19:49 So it's easier for us to call them later on.
00:19:51 So you'll want to do something like import account from dot slash account dot model.
00:20:00 And you want to repeat this multiple times.
00:20:02 Same thing for the user and so on.
00:20:05 But in this case, I'll actually go from top to bottom.
00:20:08 So we have import answer.
00:20:11 from dot slash answer model next we have import collection from collection model we have import interaction from interaction model import question from
00:20:28 question model import tag question from tag question model import tag from tag model import user from user model and import vote from vote model and finally
00:20:45 you want to export all of them together like this within a single object so far we haven't done anything to fix that previous issue but now we can fix
00:20:54 it more easily by heading over into lib mongoose.ts and then right here at the top Right after we import the logger, we can also import at forward slash database.
00:21:09 This will basically import this entire object, which will put all of these models to use.
00:21:14 So we'll be able to fetch their data whenever we want to.
00:21:18 So all the questions will just work, even though they work right now because we have already logged in, but now it'll work in all cases.
00:21:25 Now, before we proceed, let's look into these errors.
00:21:28 It's saying that it encountered two children with the same key, ending it 09. And keys should be unique, so therefore, this is an error.
00:21:38 Is this happening with the questions?
00:21:40 I really don't think so, we have only two, which are different.
00:21:43 But if you look at the tags of the first question, it seems like the nojs tag is appearing two times.
00:21:49 which makes me think that we made a mistake somewhere in the edit question logic.
00:21:53 So let me head into the question details and just append edit right here to the URL.
00:21:59 That's going to lead us to the edit questions page.
00:22:02 And we can see that the tags have been duplicated here as well.
00:22:05 So let's go ahead and check out the logic for editing the questions, specifically editing the tags part of a question together.
00:22:12 I'll head over into question.action.ts and that will expand my edit question action.
00:22:20 And the first thing I'm noticing here is that instead of question, I should have actually passed the iQuestionDoc.
00:22:28 The difference between iQuestionDoc and iQuestion is that the iQuestionDoc also contains everything that a Mongoose document has,
00:22:36 such as created ad fields as well as underscore IDs.
00:22:40 So we want to properly validate that with TypeScript as well.
00:22:43 Not a big deal, shouldn't break anything, but always better to be more precise.
00:22:48 Next, let's head over to the tags part.
00:22:50 That is right here.
00:22:53 Looks like I have to expand this part right here where we have tags to add.
00:22:57 So what we're doing here is we're mapping over the tags, trying to find existing tags.
00:23:03 And if an existing tag exists, we want to add that one and not create a new one.
00:23:08 And it looks like I just have one small issue with this regular expression.
00:23:12 Instead of saying new regular expression, you can actually do it differently in Mongoose.
00:23:17 You can simply say dollar sign regex and then define it like this.
00:23:21 Just a string without even passing a regular expression.
00:23:24 And then the second part, the case insensitive, can be just a dollar sign options like this.
00:23:30 I think either way is fine, but since we're using Mongoose, we don't necessarily have to open up a new regular expression.
00:23:37 Mongoose will do it for us if we use proper syntax.
00:23:40 I think the issue is related to removing the tag.
00:23:43 So if we head over to the tags to remove part, here we're checking the tags belonging to a specific question and we want to filter them out.
00:23:51 I'm checking whether tags to remove includes a tag ID, but what I should have been checking for if is tags IDs to remove includes a tag ID.
00:24:01 This is the only way to do a correct comparison to get access to the correct data on which are the final tags we want to keep.
00:24:08 So I think these few changes were it, but now of course we have to test it out to know for sure.
00:24:15 If I reload, we'll still see two of the same tags.
00:24:18 So let's try to delete it.
00:24:20 Okay, I deleted one and it's not actually letting me delete a second.
00:24:23 That's okay because this code consisted of some buggy logic.
00:24:27 So now let's try to save it by clicking edit, question updated successfully.
00:24:34 And now we can head over to the edit page one more time.
00:24:37 And now we have two JavaScript tags.
00:24:39 Something is definitely not right with this edit functionality.
00:24:42 So here is what I would do next.
00:24:44 Head over to your MongoDB Atlas and go browse your collections.
00:24:48 Then, completely remove the questions collection to clear out all of the existing questions, just to make sure that we don't have any documents with some
00:24:57 duplicated tags within them.
00:24:58 So, you'll have to verify you want to delete it by retyping the name of the collection, and it's called drop, and you can click drop to drop that collection.
00:25:07 We'll do the same thing with the tags collection by dropping it, as well as with the tag questions collection.
00:25:15 So let's do the same thing here.
00:25:17 Now that we have dropped all the collections, you can see our no questions found message, which means that everything's working properly.
00:25:23 And before we create new questions, let's actually head to the code and let's try to find that sneaky bug that's causing all of this to happen.
00:25:31 We have an issue where some tags are added two times.
00:25:34 So let's head over to tags to add.
00:25:37 which is right here, and here we're actually filtering the tags to figure out which ones have to be added and which ones have to get removed.
00:25:44 In this case, I thought that questions.tags is of the same structure as tags, but it's really not.
00:25:53 It's actually an array of strings and question tags was an array of objects.
00:25:57 So we have to do another filter to properly compare the two.
00:26:02 We can do that by saying tags.filter tag And then we have to say question.tags.sum.
00:26:09 So if some of the tags here in this tags array match this.
00:26:14 So let's say t as in i tag doc, like this.
00:26:20 And then we can say t.name.includes.
00:26:24 So this is how to make a proper comparison.
00:26:27 And of course we have to close it properly.
00:26:29 And another thing we have to do here is compare the lowercase name of the tag with the lowercase tag right here.
00:26:35 So we have to say t.name.toLowerCase like this, and then that includes tag to lowercase.
00:26:44 And now we have to do a similar thing for tags to remove.
00:26:48 So let's say question.tags.filter.
00:26:52 Within it, a new callback function where we get access to a tag of i.tag.doc.
00:26:58 And we want to check if not tags.sum, where we get access to a tag, t.toolovercase is triple equal to tag.name dot to lower case,
00:27:12 something like this.
00:27:13 And I believe this should do the trick.
00:27:15 I know it's a bit more complicated, and a way you could have figured out there's an issue with this is if you console logged both the tags as well as the question.tags,
00:27:25 you would then notice that their form is different, and then you have to do some additional logic to compare the two.
00:27:31 Finally, once we do this, we can head over to question.tags where we finally decide which tags we want to add to the question.
00:27:39 We have to do a similar thing.
00:27:41 Here, we're saying question.tags.filter where we get a tag ID and then we need to say not tag IDs to remove dot sum.
00:27:50 So we're only trying to figure out if some of them don't match and we can say ID of a type mongoose dot types.
00:28:05 And finally, instead of calling this a tag ID, it's actually a tag, so that was my bad because question.tags contains an array of tags,
00:28:14 not tag IDs.
00:28:16 Once we do this, I believe we should be good.
00:28:19 And once again, if something is unclear, try to compare the differences that we have now made to this file.
00:28:25 You can see them right here in the diff.
00:28:27 And then put some cons and logs where you can compare the differences between question.tags and tags.
00:28:33 And you'll immediately be able to figure out why we had to do additional comparisons to compare the two.
00:28:39 Now let's actually create a new question to test whether everything works.
00:28:44 I'll use a similar one I had before.
00:28:46 How does Node.js event loop work?
00:28:49 I'll also copy the detailed explanation as well as add tags of Node.js and JavaScript.
00:28:57 And I'll click ask a question.
00:28:59 That should work without problems.
00:29:02 And it does.
00:29:03 And now we can head over to the edit page.
00:29:06 Or before that, let's actually go to the homepage to make sure that we can see it.
00:29:11 And we can, this is great, 14 seconds ago.
00:29:15 I can now head over to the edit page and I'll try to remove one of the tags, such as Node.js.
00:29:20 Click edit.
00:29:21 Okay, that works.
00:29:22 Only JavaScript is there.
00:29:24 And now I'll try to bring back the Node.js tag, as well as maybe TypeScript tag, just to try to stress test it.
00:29:35 Just kidding, it should handle this without issues.
00:29:38 And if we do this...
00:29:40 You can see that now it has all three.
00:29:43 This is working perfectly, but if we encounter any more issues with tags or anything else, we'll of course fix them as they come up.
00:29:50 Great.
00:29:50 We're now successfully fetching and displaying questions on the homepage.
00:29:54 So let's see which files we had to change to make that happen.
00:29:57 First, it was the actual page.tsx, the homepage.
00:30:00 So from here, we can now remove this part where we had filtered questions because now we're fetching real questions right here.
00:30:07 and then displaying them below.
00:30:09 We also had to export all of these models just so we can very simply import them here so we can use them later on in our calls to make it work with the
00:30:17 serverless nature of our application.
00:30:19 We added the validations as well as the edit question and finally get questions server action.
00:30:25 So let's actually close these files and let's say implement.
00:30:31 fetch and display questions, commit and sync.
00:30:35 In the next lesson, we'll take the part where we're rendering those questions and we'll further optimize it.