Understanding Middleware in Express
Before we dive into creating pagination middleware, let's understand what middleware is and why it's so powerful. Think of middleware as a series of checkpoints that a request goes through before reaching its final destination. Just as a library might have a check-in desk where visitors show their cards before accessing different sections, middleware processes requests before they reach their final route handlers.
In our school supply management system, we've been implementing pagination in multiple routes. Instead of repeating this logic everywhere, we can create a single middleware that handles pagination consistently across our entire application. This is similar to how a school might have a standard procedure for processing new student registrations, rather than having each department handle it differently.
Creating the Pagination Middleware
Let's create a middleware that will process pagination parameters and add standardized pagination information to our requests. We'll build this step by step, understanding each component's purpose.
Basic Pagination Middleware Structure
// server/utils/pagination.js
/**
* Middleware to handle pagination parameters and calculations
* This middleware processes page and size query parameters,
* validates them, and adds pagination details to the request object
*/
const paginationMiddleware = (options = {}) => {
const {
maxSize = 200, // Maximum allowed page size
defaultSize = 10, // Default page size if none specified
defaultPage = 1, // Default page number if none specified
allowUnlimited = true // Whether to allow unlimited results (page=0,size=0)
} = options;
return (req, res, next) => {
try {
// Extract pagination parameters from query
let { page, size } = req.query;
// Convert to numbers and handle defaults
page = page ? Number(page) : defaultPage;
size = size ? Number(size) : defaultSize;
// Handle the special case for unlimited results
if (allowUnlimited && page === 0 && size === 0) {
req.pagination = {
limit: null,
offset: 0,
page: 1,
size: null,
unlimited: true
};
return next();
}
// Validate pagination parameters
if (page < 1 || size < 1 || size > maxSize) {
return res.status(400).json({
errors: [{
message: 'Invalid pagination parameters. Page must be >= 1 and size must be between 1 and ' + maxSize
}],
count: 0,
pageCount: 0
});
}
// Calculate offset and limit for database queries
const limit = size;
const offset = (page - 1) * size;
// Add pagination information to the request object
req.pagination = {
limit,
offset,
page,
size,
unlimited: false
};
next();
} catch (error) {
console.error('Error in pagination middleware:', error);
res.status(500).json({
errors: [{ message: 'Internal server error processing pagination' }],
count: 0,
pageCount: 0
});
}
};
};
Our middleware does several important things:
1. It accepts configuration options to make it flexible for different use cases
2. It validates and processes pagination parameters
3. It handles special cases like unlimited results
4. It adds standardized pagination information to the request object
Enhancing the Middleware with Additional Features
Now that we have our basic middleware, let's add some helpful utilities that make it even more powerful and easier to use in our routes.
Adding Pagination Utilities
// server/utils/pagination.js
/**
* Utility function to calculate total pages based on total count and page size
* This helps routes provide consistent pagination metadata in responses
*/
const calculatePageCount = (totalCount, pageSize) => {
if (!pageSize) return 1;
return Math.ceil(totalCount / pageSize);
};
/**
* Helper function to create a standardized paginated response
* This ensures consistent response structure across all paginated routes
*/
const createPaginatedResponse = (data, totalCount, pagination) => {
const { page, size, unlimited } = pagination;
return {
rows: data,
count: totalCount,
page: unlimited ? 1 : page,
pageSize: size,
pageCount: unlimited ? 1 : calculatePageCount(totalCount, size)
};
};
/**
* Enhanced pagination middleware with utility functions
*/
const createPaginationMiddleware = (options = {}) => {
const middleware = paginationMiddleware(options);
// Attach utility functions to the middleware
middleware.createResponse = createPaginatedResponse;
middleware.calculatePageCount = calculatePageCount;
return middleware;
};
Using the Pagination Middleware
Let's see how our middleware simplifies route handlers and ensures consistent pagination across our application.
Implementing Paginated Routes
// Example route implementation
const express = require('express');
const router = express.Router();
const pagination = createPaginationMiddleware({
maxSize: 50,
defaultSize: 10
});
// Apply pagination middleware to a route
router.get('/students', pagination, async (req, res) => {
try {
const { limit, offset } = req.pagination;
// Get total count and data in parallel
const [totalCount, students] = await Promise.all([
Student.count(),
Student.findAll({
limit,
offset,
order: [['lastName', 'ASC']]
})
]);
// Use utility function to create consistent response
const response = pagination.createResponse(
students,
totalCount,
req.pagination
);
res.json(response);
} catch (error) {
console.error('Error fetching students:', error);
res.status(500).json({
error: 'Failed to fetch students'
});
}
});
Testing the Pagination Middleware
To ensure our middleware works correctly in all scenarios, we should test it thoroughly. Here's how we can create comprehensive tests:
Middleware Tests
// server/utils/__tests__/pagination.test.js
describe('Pagination Middleware', () => {
let req, res, next;
beforeEach(() => {
req = { query: {} };
res = {
status: jest.fn().mockReturnThis(),
json: jest.fn()
};
next = jest.fn();
});
test('uses default values when no query parameters provided', () => {
const middleware = paginationMiddleware();
middleware(req, res, next);
expect(req.pagination).toEqual({
limit: 10,
offset: 0,
page: 1,
size: 10,
unlimited: false
});
expect(next).toHaveBeenCalled();
});
test('handles unlimited case correctly', () => {
req.query = { page: '0', size: '0' };
const middleware = paginationMiddleware();
middleware(req, res, next);
expect(req.pagination).toEqual({
limit: null,
offset: 0,
page: 1,
size: null,
unlimited: true
});
expect(next).toHaveBeenCalled();
});
test('validates maximum page size', () => {
req.query = { page: '1', size: '201' };
const middleware = paginationMiddleware({ maxSize: 200 });
middleware(req, res, next);
expect(res.status).toHaveBeenCalledWith(400);
expect(res.json).toHaveBeenCalledWith(expect.objectContaining({
errors: expect.any(Array)
}));
expect(next).not.toHaveBeenCalled();
});
});
Best Practices and Considerations
When implementing and using pagination middleware, keep these important considerations in mind:
Configuration Management
Consider storing pagination configuration in environment variables or a configuration file. This allows for easy adjustments without changing code:
// config/pagination.js
module.exports = {
maxSize: process.env.MAX_PAGE_SIZE || 200,
defaultSize: process.env.DEFAULT_PAGE_SIZE || 10,
defaultPage: 1,
allowUnlimited: process.env.NODE_ENV === 'development'
};
Error Handling
Ensure your middleware handles all possible error cases gracefully and provides helpful error messages to clients.
Performance Considerations
Remember that pagination middleware runs on every request to routes where it's applied. Keep the logic efficient and avoid unnecessary operations.
Extending the Middleware
Consider these possible extensions to make the middleware even more powerful:
Add support for cursor-based pagination alongside offset-based pagination
Implement response caching for frequently accessed pages
Add support for different response formats (JSON, XML, CSV)
Include metadata about previous and next pages in the response