# Python FastAPI Tutorial (Part 13): Pagination - Loading More Data with Query Parameters

## Метаданные

- **Канал:** Corey Schafer
- **YouTube:** https://www.youtube.com/watch?v=f1zggIOxmJg
- **Дата:** 11.03.2026
- **Длительность:** 36:32
- **Просмотры:** 3,815

## Описание

In this video, we'll be learning how to add pagination to our FastAPI application. Right now, our app returns all posts at once, which doesn't scale well as data grows. We'll fix that by adding skip and limit query parameters to our API, using SQLAlchemy's offset and limit for efficient database queries, creating a paginated response schema with metadata like total count and whether more data is available, and wiring up a Load More button on the frontend to fetch additional pages from our API. This is an industry-standard pattern you'll encounter on nearly any list endpoint in a real-world API. Let's get started...

The code from this video can be found here:
https://github.com/CoreyMSchafer/FastAPI-13-Pagination

Full FastAPI Course:
https://www.youtube.com/playlist?list=PL-osiE80TeTsak-c-QsVeg0YYG_0TeyXI

FastAPI-Pagination Library - https://github.com/uriyyo/fastapi-pagination

✅ Support My Channel Through Patreon:
https://www.patreon.com/coreyms

✅ Become a Channel Member:
https://www.youtube.com/channel/UCCezIgC97PvUuR4_gbFUs5g/join

✅ One-Time Contribution Through PayPal:
https://goo.gl/649HFY

✅ Cryptocurrency Donations:
Bitcoin Wallet - 3MPH8oY2EAgbLVy7RBMinwcBntggi7qeG3
Ethereum Wallet - 0x151649418616068fB46C3598083817101d3bCD33
Litecoin Wallet - MPvEBY5fxGkmPQgocfJbxP6EmTo5UUXMot

✅ Corey's Public Amazon Wishlist
http://a.co/inIyro1

✅ Equipment I Use and Books I Recommend:
https://www.amazon.com/shop/coreyschafer

▶️ You Can Find Me On:
My Website - http://coreyms.com/
My Second Channel - https://www.youtube.com/c/coreymschafer
Facebook - https://www.facebook.com/CoreyMSchafer
Twitter - https://twitter.com/CoreyMSchafer
Instagram - https://www.instagram.com/coreymschafer/

#Python #FastAPI

## Содержание

### [0:00](https://www.youtube.com/watch?v=f1zggIOxmJg) Segment 1 (00:00 - 05:00)

Hey there. How's it going everybody? In this video, we're going to be learning how to add pageionation to our fast API application. So, if you've been following along with this series, we now have a complete blog application. So, users can register, log in, create post, edit, and delete their own post, and we added profile picture uploads in the last video. But there's a problem with how we're currently handling our post. So, right now, we return all the posts at once. And that might not seem like a big deal now since we only have one post here or a handful of posts if you've created a few more. But if we have hundreds or thousands, then it's going to be slow, wasteful, and a poor user experience. So in this tutorial, we're going to fix this by adding pageionation. So we'll add query parameters for skip and limit to our API. We'll do database pageionation with SQL Alchemy. And we'll add a load more button here on the front end. And this is really an industry standard pattern that you'll see on almost any list endpoint in a real world API. Now when you hear pageionation, you might think of it as a front-end concern, but here it's going to be actually driven by our back-end API. The API is going to be what controls how much data gets sent per request and the front end just consumes whatever the API gives it. So the bulk of what we're going to be doing here is going to be on the API side and then we will wire up the front-end side to use it. All right. So first things first, we need enough data to actually test pageionation because it's hard to test pageionation if we only have a few posts. And we can see here I only have one post. So I have a script included in the tutorial files called populate db. py here. And I have that open in my editor right now. Let me quickly show you what it does before we run it here. So, this script creates a handful of users, assigns them profile pictures from this populate images directory here, and generates 44 sample posts with realistic dates spread out over a few months. It's just a utility to get us data to work with. I'm not really going to walk through uh how I wrote this script since it's not the point of the tutorial, but it is going to be available in the tutorial files if you want to look at this. And as usual, everything's going to be available for download in the description section below. So, I wanted to make this as easy for you all to run as possible. So, if you have the same basic setup that I've been using, then you should just be able to run this script directly without any additional options. So let me give a very brief broad explanation of this script really quick. So at the bottom when we run this script, it runs this populate function here. If I look at this, then I actually wanted these to go through our API instead of just populating the database directly. So what we're doing here is spinning up a client. The first thing we do is clear existing data and then I am looping over this list of users here. If I look at this, then we can see that I just have some sample users, emails, passwords that are all just tests. And then I have my sample post right here. So, let me go back to where I was here in the populate script. So, it's looping through our list of users. It creates uh each one through our API. It logs them in and then gets an access token from them and then it uploads their profile picture and then if I scroll down here then it loops through that list of posts and creates those and it distributes those post across different users and then finally it updates all of the post all the post dates so that they're spread out over the last few months so that it looks more realistic. So here it's where it's creating those posts. And then at the bottom here, uh this update post dates function. This is what uh updates that time delta so that those are spread over the last few months. So I'm not going to go over that in too much detail. Let me stop the server that I'm running here. And now let me run this populate script. Now, just a heads up, this script will clear out everything that's currently in the database and replace it with our sample data. So, make sure that's what you want before running it if you're following along here. So, it also uses the profile uh images from this populate images directory and the downloadable code. So, you'll need those images as well. You can swap in your own images if you'd like by updating the users list in the script. Okay, so I'm going to go ahead and run this. Again, I'm using UV, but if you're using a normal uh virtual environment, then you could run this with that version of Python. But I'm going to say uv run python. And then this will be populate db. py. So, let me run that. And we can see here that it

### [5:00](https://www.youtube.com/watch?v=f1zggIOxmJg&t=300s) Segment 2 (05:00 - 10:00)

says that it created all of these users here. And then it tells us that it created those posts and that we have six new users and 44 posts. So now if I go back here and actually I need to run our server again. So let me do that. So that was just UV run fast API dev. Spin that up. Go back to my browser here. Let me reload this homepage. And now we can see that we have a lot more posts here. And it's spread out with a few different users here. So we can see that all 44 of those posts are being loaded in on the page at once. And this is just with 44 posts. So imagine if we had a lot more. Uh this would become slower and we'd be sending a ton of data over the network that the user doesn't even need yet. So that's what we're going to be fixing here with pageionation. All right. So now that we have that sample data, we can go ahead and start implementing pageionation here. So the first thing that we need to do is define what our pageionated response is going to look like. And this is the contract between our API and the front-end client. So right now our get post endpoint uh just returns a list of posts. But we really need to return more information than that. So let me show you what I mean here. So in routers I'm going to go to post here. And now let me go to the get post route. So that is get post. And we can see here that we are just returning a list of this post response and these are within our schemas here. So now let me open up our schemas and let's take a look at these. So this post response here is what that is returning. But we need more information than that. So we need metadata about the pageionation like how many total posts exist and whether there are more to load. So now that we're in schemas. py here at the bottom of the file uh after our post response class here I'm going to add in a new class for our pageionated response and just so you don't have to watch me type this out I'm going to grab this from my snippets but we'll explain exactly what we're adding after I paste this in so if I go to my snippets here let me grab this new class that we're going to be using for our schemas and now let's go over this so we have this pageionated post response and that inherits from the base model. Now this post field is the actual list of post response objects and that's going to be the post data. And then we have a total here which is an integer and that's going to be the total count of posts in the database. And then we have skip which is the current offset. We have limit which is how many posts were requested. And then finally we have has more which is a boolean that tells us if there are more posts after this batch. Now you might wonder why we need this has more when we also have total. And the reason is that it makes the front-end code simpler. So the front end can just check this has more boolean here and know whether to show a load more button or not. So they don't have to do any calculations on their side. So the API just tells them if there are more posts and it just makes it easier for the client. Okay. So I'm going to save that. And now let's update our post endpoint to actually use this pageionation. So I'm going to go back to our posts in routers here. And again just to show you where that was. That was in routers and it's this post router here. So now at the top I need to update my imports here. So the first import here is for fast API. We are going to include query and my imports auto sort whenever I save. So if you see these jump around then that is why but you can see the unused ones here are underlined. Now I'm also going to have to add this funk import here to SQL alchemy. And then I also need to add that pageionated post response uh from my schemas. So here from the schemas I'm going to add that pageionated post response there and save that. So now let's update this get post endpoint here since this is the main point of the tutorial. Uh I want to walk through these changes step by step instead of posting pasting them all in as snippets. So let's go through this a bit at a time here. So first we're going to change the response model because right now it's a list of post responses but we want to return our new pageentated schema instead. So I'm going to change this uh response model here to be a pageionated uh post response. And next we need to add query parameters here for skip and limit. Right now the

### [10:00](https://www.youtube.com/watch?v=f1zggIOxmJg&t=600s) Segment 3 (10:00 - 15:00)

function just takes in this database session here. And I'll add skip and limit as additional parameters. So I am going to paste in just a couple of these from my snippets just so I don't mistype anything here. So let me grab this part. these additional parameters here and I will paste those in. So for skip here we're using this annotated syntax with query to add constraints. So for skip query is greater than or equal to zero and that's just because we don't want negative offsets and the default is going to be zero. So if no skip is provided then we just start at the beginning. Now for limit here we have a query of greater than or equal to one and less than or equal to 100. And that just means that we don't want someone requesting zero or negative post. And we cap it at 100 to prevent someone from, you know, requesting a million post and just exhausting all the resources and grabbing everything. And let's just set this default equal to 10, which I think is a reasonable uh batch size for how many posts you want. Now, you might wonder why we use skip and limit instead of something like page and per page. And the answer is that skip and limit is a little more flexible. So with skip and limit you can request any arbitrary range. So skip 20 limit 10 gives you post 21 through 30. And something like page three with per page 10 is essentially the same thing but skip and limit is more common in REST APIs and gives the client a bit more control. All right. Okay. So now inside the function here before we fetch our posts uh we need to know the total count so that we can calculate uh whether there are more posts to load. So above the existing query I'll add a count query here and I'm going to grab these from the snippets as well. So let me grab these and paste this in here. Let me get the indentations correct. So here we're just doing a database query and we are using this select funk count here and then selecting from models. post to get a count of all the posts in the database. And we don't need order by here since we're just getting a total number. And then for total here we're saying that we just want that result. If there isn't a result then we just set that total equal to zero. Now for the existing query here we don't really need to change much. We just need to add an offset and a limit and that will be our skip and limit. So after our order by here, I'm just going to do a offset and that will be our skip. And let me correct that typo there. And then this will be a limit of limit here. And this actually is part of this select here. So I need to remove that comma and add it there. And these are all chained together here. So it's still giving me a syntax error because I have a comma there as well. Now if I save that, you can see that all clears up. So the offset here tells the database to skip that many rows before returning results. And limit tells it to return at most that many rows. And notice that the order by is still here. So this is really important for pageionation. If you don't order your results, the database can return rows in any order, which means uh the same skip and limit values could give you different results on different requests. So we order by date posted in descending order. Uh so we always get the newest post first and the pageionation stays consistent. Now after we get our posts here uh we need to calculate has more. So the logic is if skip plus the number of posts we got is less than our total then there are more posts to fetch. So for example if we skipped 20 and got 10 post then that's 30 posts that we've seen so far. So if total is 44 then yes there are more posts. So let me go ahead and just write this out and that might be more clear here. So for has more we'll say skip plus the length of posts and that is how many posts were returned. If that is less than our total then has more is going to be true and if it's not then that is going to be false. Uh, and finally, instead of just returning the list of posts here, we're going to return the pageionated response with all the metadata. So, this is going to be a little different than what we have here.

### [15:00](https://www.youtube.com/watch?v=f1zggIOxmJg&t=900s) Segment 4 (15:00 - 20:00)

Let me actually grab this from snippets here since there's a good bit of metadata to look at, but I will go over all this here. So, instead of returning just all those post, we are going to return that pageionated response here. So here we're returning this uh pageionated post response. Now notice this line where we do uh post response. Validate post uh for post and post. Now normally when fast API handles the response model, it does this conversion automatically. But here we're constructing the response object ourselves. So we need to handle the conversion manually. So this ensures that all the nested relationships like author are properly serialized. And that should be everything that we need for that route. So let me save this. And now let's test this in the API docs. So let me make sure that the server is still running. It should be. And let's go to our docs here. And now let me find the get API uh post route. So it is right here. Let's expand that. And now we can see that we have these new query parameters here of skip and limit. So let me try that with the default values first. So I'll go to try it out. And now let's click execute here. And we can see that we got a 200 success response. And this response is going to be that new pageionated post response. So we can see that we have our list of posts here. If I go below here, then we can see that it also gives us the total of 44. Our skip was zero since we used the default. Limit was 10 since we have the default and has more is equal to true. So basically we got the first 10 posts and there are more to load. And if I just eyeball this 1 2 3 4 5 6 7 8 9 10. Yep. So it looks like 10 posts. So now let me try up here uh a skip of let's say 10 and the limit of 10 and let's execute that and now these are different posts here so these are the next 10 posts and if I go to the bottom here we can see our total skip limit has more is equal to true here uh since we haven't seen all 44 yet so now let's actually exhaust all of these So since there are 44 posts, if I skipped 40 of them and then tried to get 10 more and execute that, then we can see here that we only have 1 2 3 4 uh and then they're exhausted. So we have a total of 44, skipped 40, we tried to get 10 has more is equal to false. And now let me also try some invalid values here uh just to show that the validation is working. So for example, if I try a limit of 200, which would load 200 posts at once, we should get an error because we set the max to 100. And we can see that there. So for limit value must be less than or equal to 100. And I'm not sure if that returns that. Uh it doesn't tell us that it's a uh 422 error. Uh but that is what that is if we actually try to do that. And again, I haven't been showing this much throughout the series. Uh, but if you wanted to actually test this in your terminal, then you can copy the curl command here. If I go to my terminal and I run this, uh, then that is actually going to work there. So, that is with the skip of 40 and limit of 10. Uh I thought it was going to give me the curl command uh for that limit of 200 but it looks like it didn't even uh allow me to get past the form here uh since that is not a valid option there. So those query constraints are working well. Uh so the pageionation on the API side is done and for the frontend side we won't have much custom logic. So we're going to let the API do its thing and just request the pages that we want. So now our API is pageionated but our homepage is still loading all of our post. So if you are if you remember our homepage route in main. py does its own database query directly. So even though the API endpoint at API post is now pageionated the homepage template route is still fetching every post from the database and rendering them all. So let me show you what I mean here. If I reload our homepage here and scroll down, you can see that we are still getting all of these posts here. So, we need to update this template route to only load the first batch and then we'll use JavaScript to load more from the API

### [20:00](https://www.youtube.com/watch?v=f1zggIOxmJg&t=1200s) Segment 5 (20:00 - 25:00)

when the user clicks a button. So, let's go ahead and do that. Uh but before we update the route, let's centralize the post per page setting so that we can change it in one place. So this is the same pattern that we used for max upload size bytes in the last tutorial. Uh so that is just going to be in config. py. We can see in the last tutorial where we set the max upload size bytes there. And all we're going to do here is we are going to make a setting called post per page. Uh this will be an integer and we'll set a default equal to 10 there. Okay. That way if we want to change the page size later then we can just update it in one place. Now let me go ahead and open main. py here. And now let's update that home route. Uh but first I need to update my imports. So I need to add the funk to SQL alchemy here. So right after select I will add in funk and then I need to import settings from config. So at the bottom of the imports here I will say from config uh we want to import our settings. And again that auto sorted the imports whenever I saved but you can see the underlined ones there are the new ones that we added. And now let me update the home route. So let me find the home route here. So right now it just loads all posts. So we're going to do the same pageionation logic that we did in the API, but this time we're only getting the first batch for server side rendering. Uh so since we already walked through this logic step by step in the API route, I'm going to grab this one from my snippets and then we'll go ahead and walk through it. So, let me go to my snippets here and this time I'm just going to grab the entire route here and let me replace our home route here. And now let's walk through this. So, it's the same pageenation logic as the API here. So, we have the count total, the total, and then we are fetching the first batch using our post per page uh setting here. Now the difference is that there's no offset since this is always going to be the first page. Uh and then we calculate the has more boolean here. Again there's no offset or no skip uh so we don't need to add that uh to our length of post there. And then we return the template and pass has more to the template so that JavaScript knows whether to show the load more button. So this is going to give us a bit of a hybrid approach here. So the first batch is going to be server side rendered uh so that the page loads fast and search engines can see the content and then subsequent batches are fetched by JavaScript. So it's going to be a fast initial load and then uh some dynamic loading after that. Now before we update the template uh we need a couple of utility functions for our JavaScript. So when we render posts on the server with Ginga 2, Ginga automatically escapes dangerous characters in user content. But when we inject content via JavaScript, we have to do it ourselves. So I'm going to add a JavaScript utility for this. So first, let me open up our utils js JavaScript file here. And we already have some utilities uh from earlier tutorials, but I'm going to add two new functions here for pageionation. And I will put these uh right at the bottom of the file here. And again, just like I've been doing throughout the series, I'm not going to explain the JavaScript and HTML as much as the Python code since these tutorials are more focused on fast API. But let me grab these from my snippets and we will briefly explain uh what's going on here. So, let me grab these two new utility functions here and paste these in. So, the first new function here is escape HTML. So, this prevents cross-sight scripting attacks, which is where someone puts malicious JavaScript in their post title or content and it runs in the user's browser. Uh the way it works is we set the text using this text content here which treats everything as plain text and then read it back with enter HTML which gives us the escaped version. And the second function here is format date. So the API returns dates as iso strings. So for example a version of that string uh let's see here. Yeah, we can see in my terminal here, uh, an ISO string looks kind of like this here. And we need to format that to match what our

### [25:00](https://www.youtube.com/watch?v=f1zggIOxmJg&t=1500s) Segment 6 (25:00 - 30:00)

front end is producing with Ginga 2, uh, which is just the month, day, year. And that is what this JavaScript function here is doing. So both of these functions are exported so that we can import them into our templates. So now let's update the home template. So I'm going to look at our templates here. Let's go to home. html. And for the content block here, we just need two small changes. So the article markup uh inside the for loop stays the same. But first, I need to wrap this for loop in a div with an ID so that JavaScript can find this div and append new post to it. So right above the for loop here, I'm going to add a new div. So I will say div and we will just set this with an id equal to uh post container and then right after our for loop. So the end for here we will just close off that div. And second I need to add a load more button below that post container that we just created. Uh so this will only render if has more is equal to true. Let me grab this from my snippets. This is pretty small here. So, let me paste this in. So, if there are more posts to load here, then we just show this button of load more post. So, if there are not more posts, then it doesn't render at all. Now, for the JavaScript at the end of this file, I'm going to add a scripts block here. Now, this is a big chunk of JavaScript. So, I'm going to paste this from my snippets and then walk through what it does. So, let me grab this scripts block here and we will paste this in. All right. So, there's a lot here. So, let's briefly walk through this here. So, at the top here, we're initializing our pageionation state uh using ginga 2 to inject the server rendered values. So current offset here starts at limit because the server already rendered the first batch. So we know to start fetching from there and then has more tells us whether to even bother with adding in the load more button. Now this create post HTML function builds the HTML for a single post. So this has to match the structure of the server rendered post exactly and we use escape html uh in here with all of the user content and format date here for the dates. Now the load more post function here this is basically going to be the main logic uh where we are using our API. So it fetches from our pageionated API using the current offset and limit. So you can see that's what it's doing here. It is fetching from our API with that current offset and that limit. And then we just have some uh error handling here. And then we're just appending those new posts to our container. And then we update the current offset here using that post. length. If there are no more post after we fetch the next batch, then it hides that load more button. And then if there's an error, then the button changes here to uh error click to retry so that the user can try again. Okay, so let me save this. And now let's try this out. So if I go back to our homepage here and load this and it's going to load in that first batch and this should be 10 posts here. So 1 2 3 4 5 6 7 8 9 10. And then we can see that we have the load more post button here. If I click on that, then we can see that it dynamically talks to our API and gets that next batch of 10 posts. And we can see that the button is still there because there are still more posts. So I could click that again. More posts loaded in. And I could keep going down here until we are done. And then we hit the bottom and there are no more posts uh for us to load in. So the button does not show. Now we should apply this same pattern to user posts as well. Uh so what I mean by that is if I click on a user then we can see that we have a page for user posts as well. Uh we do not have pageionation on the these pages yet. So we should add that in. Now, this is going to be the exact same pattern, but just filtered by a specific user's post. So, let me update uh the API endpoint for that. So, to do that, I'm going to look in my routers here, and this is going to be in the users router. And let me scroll up to the top here.

### [30:00](https://www.youtube.com/watch?v=f1zggIOxmJg&t=1800s) Segment 7 (30:00 - 35:00)

So, this is going to be very similar to what we've already seen. Uh we need to add query to our imports here for fast API. And we also need to add pageionated responses from our schemas. So if I find my schema here, then that is going to be pageentionated post response. And now let me find that get user post endpoint. So get user post endpoint that is right here. Now since this is all stuff that we've already seen, I will just grab this from my snippets here. So let me just grab this entire new route here. This is all stuff that we have seen already but let's take a brief look here. Now this is the exact same pattern that we saw in the main uh get post endpoint. The only difference so we can see that we have pageionated post response. We have the skip the limit and we are doing the count and the total and things like that. The only difference is that we are filtering by user ID in both the count query and the select query. So you can see here that we're using this where uh the post user ID is equal to user ID to only count and fetch post by that specific user. So that's good for the API side. So now I need to update the template and all of our templates here are in the main. py. So if I scroll down to user post page here, we are going to need that same pageionation logic here. So count limit has more things like that. Let me grab that from my snippets here. And we will replace that entire route. So that same pattern again, we're getting the count filtered by the user ID. We get the first batch of post. Then we calculate has more and we pass limit and has more to that user post. html template. And then lastly, I just need to update that user post template. So let me go to that template. So that is going to be user post. Now this template is almost identical to home. html but with two small differences. There's a heading at the top showing whose post we're currently viewing and the JavaScript is going to fetch from a different API URL uh that includes the user ID. Again, since this is basically identical to what we've already seen, I will just grab all of this from the snippets and we don't really need to explain much what's going on here since we have already looked at this. So, I'm just going to replace that entire template there. So the structure here is basically the same as home. html. The only real differences are is that everything is user specific instead of getting all of the posts. Now in order for testing this, I actually probably should have added a user that has more than 10 posts uh because I don't think I do have that in my sample data. So, you're just going to uh have to believe me here when I say that this is working. We can kind of get an idea of what it looks like when the load more button isn't there uh whenever our initial batch is less than what would cause the load more button to show up. Okay. So, let me go back to our home route here. And actually, just to uh test this out, let me also look here. I'm going to go to get users post here and try this out. Let's get user ID of one. I believe that is my Corey MS user. It is. And if I scroll to the bottom of these, then we can see that it's working on the API side as well. We have our total, we have our limit and has more is false there because there are only nine total posts. So there are no more to go after we fetch the first 10. Okay. So before we wrap up, I should also mention that there's a library called fast API pageionation that handles a lot of this boilerplate for us. It supports multiple pageionation strategies and gives consistent response formats. But for learning, I think that manual implementation like we did here is better so that we understand what's happening. But for production, it's worth evaluating if the library saves you time versus adding complexity. That's just a uh decision that you'll have to make once you take a look at it. And I'll leave a link to that package in the description section below if anyone is interested. So to recap what we did here, the main thing is that we added skip and limit query parameters to our

### [35:00](https://www.youtube.com/watch?v=f1zggIOxmJg&t=2100s) Segment 8 (35:00 - 36:00)

API with validation. We added in that new pageionated postresponse schema. We used SQL Alchemy's offset and limit for database level pageionation. And then we wired all of that up to the front end using that load more button that fetches additional pages from the API. So now we've got a real nice pageionated system here uh on the API side that the front-end users will be able to use however they see fit. Now, in the next video, we're going to be covering uh password reset functionality. So, we'll learn how to send emails with background tasks, how to create secure reset tokens, and we'll complete that password reset placeholder that we've had sitting here. But if anyone has any questions about what we covered in this video, then feel free to ask in the comment section below, and I'll do my best to answer those. And if you enjoy these tutorials and would like to support them, then there are several ways you can do that. The easiest way is to simply like the video and give it a thumbs up. Also, it's a huge help to share these videos with anyone who you think would find them useful. And if you have the means, you can contribute through Patreon or YouTube. And there are links to those pages in the description section below. Be sure to subscribe for future videos. And thank you all for watching.

---
*Источник: https://ekstraktznaniy.ru/video/11649*