Efficient List Endpoints in Lumen: Mastering Eloquent Pagination
Learn how to enable Eloquent pagination in Lumen, build JSON list endpoints that respect limits, and choose between paginate() and simplePaginate() for performance.
17 Aug 2025, 18:20 UTC

Why Pagination Matters in Lumen
When you expose a database table as a JSON endpoint, the first instinct is to return all rows. In a micro‑framework like Lumen, that can quickly become a performance nightmare: a single request might fetch thousands of records, saturate memory, and overwhelm the client. Pagination solves this by slicing the data into manageable chunks and, more importantly, by giving the API a predictable envelope that front‑ends can consume.
Step 1 – Turn on Eloquent
Lumen ships with Eloquent disabled to keep the footprint small. Pagination lives on the Eloquent query builder, so you must enable it early in the bootstrap process.
// bootstrap/app.php
$app = new Laravel\Lumen\Application(__DIR__.'/../');
// Enable Eloquent ORM and Facades
$app->withEloquent();
$app->withFacades();
return $app;
Without these two calls, paginate() and simplePaginate() are undefined. After enabling Eloquent, the Illuminate\Pagination classes are available to any model or query builder instance.
Step 2 – Pick the Right Pagination Strategy
Lumen offers two built‑in helpers:
paginate($perPage)– performs aCOUNT(*)to determine the total number of rows, then fetches the current page. The JSON envelope includestotal,per_page,current_page,last_page, anddata.simplePaginate($perPage)– skips the count query, returning onlynext_page_urlandprev_page_urlalong with the data. This is ideal for very large tables or infinite‑scroll UIs where the total count is unnecessary.
Both helpers automatically read the page query parameter. If you need a different parameter name, pass it as the fourth argument: paginate($perPage, ['*'], 'page', 'p').
Step 3 – Enforce a Safe per_page Limit
Clients can request arbitrarily large page sizes, which defeats the purpose of pagination. A common guard is to cap per_page to a maximum value, such as 100. The following snippet shows a pragmatic way to apply this rule in a controller:
// app/Http/Controllers/ArticleController.php
public function index(Request $request)
{
$perPage = (int) $request->input('per_page', 15);
$perPage = min($perPage, 100); // Enforce max 100
$articles = Article::orderBy('created_at', 'desc')
->paginate($perPage);
return response()->json($articles);
}
Because the paginator serializes itself, the JSON response already contains all the metadata you need. No extra code is required to build the envelope.
Step 4 – A Working Endpoint Example
Below is a minimal, self‑contained example that demonstrates the entire flow. The code assumes a fresh Lumen project with a seeded articles table.
// routes/web.php
$router => get('articles', 'ArticleController@index');
// app/Http/Controllers/ArticleController.php
class ArticleController extends Controller
{
public function index(Request $request)
{
$perPage = min((int) $request->input('per_page', 15), 100);
$articles = Article::orderBy('created_at', 'desc')
->paginate($perPage);
return response()->json($articles);
}
}
When a client calls /articles?page=2&per_page=20, the response looks like:
{
"current_page": 2,
"data": [
{ "id": 21, "title": "…", "created_at": "…" },
…
],
"first_page_url": "http://api.example.com/articles?page=1",
"from": 21,
"last_page": 5,
"last_page_url": "http://api.example.com/articles?page=5",
"next_page_url": "http://api.example.com/articles?page=3",
"path": "http://api.example.com/articles",
"per_page": 20,
"prev_page_url": "http://api.example.com/articles?page=1",
"to": 40,
"total": 100
}
Notice the total and last_page fields: they come from the LengthAwarePaginator generated by paginate(). If you switched to simplePaginate(), these fields would be absent, and the client would rely on next_page_url to keep requesting.
Trade‑offs & Limitations
- COUNT Cost:
paginate()runs aSELECT COUNT(*)on the same query. On tables with millions of rows or complexWHEREclauses, this can dominate response time.simplePaginate()avoids it, but you lose total‑page information. - Deep Offsets: Requesting a high page number forces the database to skip many rows, which can be slow. Cursor‑based pagination (available in later Lumen releases) mitigates this by using a unique key instead of an offset.
- Framework Support: Lumen’s release cadence lags behind Laravel. Verify that your PHP version is still supported before starting a new project that relies on pagination features.
- Memory Footprint: Even with pagination, fetching a large
per_pagecan exhaust memory. Enforcing a hard cap, as shown earlier, protects the server.
Actionable Next Steps
- Enable Eloquent in
bootstrap/app.phpif you haven’t already. - Choose
paginate()for APIs that need total counts; usesimplePaginate()for infinite‑scroll UIs or large datasets. - Implement a per‑page cap in your controller to avoid runaway queries.
- Test the endpoint with realistic data (e.g., 1M rows) to compare performance between the two methods.
- Consider cursor‑based pagination in future Lumen versions if you expect very deep pagination.
With these steps, you’ll have a robust, production‑ready list endpoint that scales, respects client limits, and delivers a clean JSON envelope.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.