To paginate Eloquent models together with their relationships in Laravel 13, chain with() and paginate(): Post::with('author')->paginate(15). Eager loading runs one extra query for all authors on the page, instead of one query per row, and the paginator still counts and slices the parent rows correctly.
This guide shows the controller, the Blade view, filtering by a relationship, keeping query strings, and when to switch to cursor pagination.
What do I need first?
- Laravel 13 (current stable at the time of writing) and a supported PHP version.
- Two related models, here
PostandUser, withPost::author()defined as abelongsTorelationship.
// app/Models/Post.php
public function author(): BelongsTo
{
return $this->belongsTo(User::class, 'user_id');
}
public function comments(): HasMany
{
return $this->hasMany(Comment::class);
}How do I paginate with eager loading?
use App\Models\Post;
use Illuminate\View\View;
public function index(): View
{
$posts = Post::query()
->with('author:id,name')
->withCount('comments')
->latest()
->paginate(15);
return view('posts.index', compact('posts'));
}Three details matter here:
with('author:id,name')loads only the columns you need. Always include the primary key (id) in the list.withCount('comments')adds acomments_countattribute without loading every comment.paginate(15)runs a count query plus a limited select. Eager loads then use the 15 loaded parents.
How do I show the results in Blade?
<table>
<thead>
<tr><th>Title</th><th>Author</th><th>Comments</th></tr>
</thead>
<tbody>
@foreach ($posts as $post)
<tr>
<td>{{ $post->title }}</td>
<td>{{ $post->author->name }}</td>
<td>{{ $post->comments_count }}</td>
</tr>
@endforeach
</tbody>
</table>
{{ $posts->links() }}By default the page links use Tailwind markup. If your layout uses Bootstrap 5, call Paginator::useBootstrapFive() inside AppServiceProvider::boot().
How do I filter by a relationship and keep the filter on page 2?
public function index(Request $request): View
{
$posts = Post::query()
->with('author:id,name')
->when($request->filled('author'), function ($query) use ($request) {
$query->whereHas('author', fn ($q) => $q->where('name', 'like', '%'.$request->string('author').'%'));
})
->latest()
->paginate(15)
->withQueryString();
return view('posts.index', compact('posts'));
}withQueryString() appends every current query parameter to the page links. Without it, clicking "2" drops the author filter. Use appends(['sort' => 'newest']) when you want to add specific values yourself.
Can I paginate a relationship of one model?
Yes. A relationship method returns a query builder, so call paginate() on it.
$comments = $post->comments()->with('user:id,name')->latest()->paginate(10);If a page has two paginators, give each its own query string name with the third argument to avoid clashes: paginate(10, ['*'], 'comments_page').
How do I return paginated data from an API?
Returning a paginator from a route converts it to JSON with data, current_page, last_page, total and link fields. To shape each item, wrap it in an API resource.
return PostResource::collection(
Post::with('author')->latest()->paginate(15)
);paginate, simplePaginate or cursorPaginate?
- paginate shows page numbers and the total, at the cost of an extra count query.
- simplePaginate shows only Previous and Next and skips the count query.
- cursorPaginate uses a "where" clause instead of an offset and suits large tables and infinite scroll. It needs a unique, ordered column and cannot show page numbers.
What are the common mistakes?
- Forgetting
with(). Every row then triggers a lazy-load query for the author. - Using
get()then slicing. This loads the whole table into memory. - Filtering in PHP after paginate. The total and the page sizes become wrong. Filter in the query.
- Selecting columns without the key.
with('author:name')breaks the relationship match. - Losing the filter on page 2. Add
withQueryString().
How can I verify the number of queries?
Enable the query log in a test or in tinker and count the statements. With eager loading you should see the count query, the page query and one query per eager-loaded relationship.
DB::enableQueryLog();
Post::with('author')->paginate(15);
dump(count(DB::getQueryLog())); // 3 queriesA feature test can also assert the page contents: $this->get('/posts?page=2')->assertOk();.
FAQ
Does eager loading run for each page?
Yes. It runs only for the rows on the current page, so the cost stays small.
Why does paginate() run a count query?
It needs the total number of rows to compute the last page.
How do I change the page size from the URL?
Read $request->integer('per_page', 15), clamp it with min() and max(), and pass it to paginate().



