Implementing Safe Pagination and Query Limits in FeathersJS
Learn how to implement safe pagination in FeathersJS v5 to prevent server crashes and protect your database from unbounded queries using limit, skip, and query whitelisting.
13 Aug 2025, 00:32 UTC

The Risk of Unbounded Queries
When an API endpoint returns a collection, the default behavior of many database adapters is to return every matching record. As a dataset grows from hundreds to hundreds of thousands of rows, a simple GET /messages request can lead to memory exhaustion (OOM) on the server or a browser crash on the client. The solution is to enforce pagination at the service level, ensuring the server never attempts to serialize more data than the infrastructure can handle.
Prerequisites
- A FeathersJS project (v5 Dove) initialized with TypeScript or JavaScript.
- A registered service connected to a database adapter (e.g.,
feathers-knexorfeathers-mongodb). - Administrative access to the server to restart the application after configuration changes.
Configuring Service Pagination
Pagination in FeathersJS is enabled by passing a paginate option during service registration. This transforms the return value of the find method from a plain array into a pagination object.
// src/services/messages/messages.service.ts
import { ServiceOptions } from '@feathersjs/feathers';
const options: ServiceOptions = {
paginate: {
default: 10, // Returns 10 items if $limit is omitted
max: 50 // Caps the $limit request to 50 items
}
};
// Register the service with these options
app.use('/messages', new MessageService(options));
By setting a max value, you prevent "denial of service" scenarios where a client requests $limit=1000000 to intentionally slow down the database.
Handling Client-Side Query Parameters
Once pagination is enabled, clients use specific query operators to navigate the data. These are passed as URL parameters in REST or as part of the query object in Socket.io.
| Operator | Purpose | Example |
|---|---|---|
$limit |
Sets the number of records to return. | /messages?$limit=20 |
$skip |
Offsets the starting point of the result set. | /messages?$skip=20 |
$sort |
Determines the order of results. | /messages?$sort=-createdAt |
Securing Queries with Whitelisting
Allowing clients to pass arbitrary query objects into params.query can be dangerous. An attacker might use expensive operators (like $regex in MongoDB) to spike CPU usage. In Feathers v5, you should use a query syntax helper or a schema to restrict allowed operators.
// Example: Restricting query operators in a hook
app.service('messages').hooks({
before: {
find: [
async (context) => {
const allowedOperators = ['$limit', '$skip', '$sort', '$select'];
const query = context.params.query || {};
// Remove any key that starts with $ and isn't in the whitelist
Object.keys(query).forEach(key => {
if (key.startsWith('$') && !allowedOperators.includes(key)) {
delete query[key];
}
});
return context;
}
]
}
});
Verification and Testing
To verify that your pagination and limits are functioning correctly, perform the following checks using a tool like cURL or Postman:
- Check Response Shape: Request
GET /messages. Confirm the response is an object containingtotal,limit,skip, anddata(the array of records), rather than just an array. - Test the Max Limit: Request
GET /messages?$limit=100. If yourmaxis set to 50, the response should showlimit: 50and contain no more than 50 items. - Verify Offset: Request
GET /messages?$limit=10&$skip=10. Confirm that the records returned are the 11th through 20th records in the collection. - Test Operator Stripping: Send a request with an unauthorized operator (e.g.,
/messages?$where=...). Confirm the server ignores the operator or returns an error, depending on your hook implementation.
Rollback and Recovery
If pagination causes issues with legacy clients that expect a plain array, you can disable it by setting paginate: false in the service options. Warning: Only do this for small, static collections. For large tables, disabling pagination may cause the application to crash under load.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.