Implementing DataTables Server‑Side Processing: Configuration, Gotchas, and Verification
Learn how to enable DataTables server‑side processing, map client parameters to a safe backend query, and verify the draw, total, and filtered counts are correct.
25 Jun 2026, 09:32 UTC

Useful answer
Enable DataTables server‑side mode by setting serverSide: true, pointing ajax to a JSON endpoint, and making that endpoint echo the draw value while returning correct recordsTotal and recordsFiltered counts. This lets the table delegate sorting, paging, and searching to the backend instead of loading the whole dataset into the browser.
Worked configuration and mechanism
The minimal client‑side setup looks like this (run after the page DOM is ready, typically in a <script> block or a module):
$('#example').DataTable({
serverSide: true,
ajax: {
url: '/api/employees', // endpoint that returns server‑side JSON
type: 'GET'
},
columns: [
{ data: 'id' }, // must match the key in each row object
{ data: 'first_name' },
{ data: 'last_name' },
{ data: 'position' },
{ data: 'salary' }
]
});
When the user pages, sorts, or types in the search box, DataTables sends an AJAX request with these parameters (shown here as query‑string for clarity):
draw– incrementing counter that the server must echo back unchanged.start– zero‑based index of the first record to display.length– page size (number of records to return).order[0][column]– index of the column to sort.order[0][dir]– sort direction, "asc" or "desc".search[value]– global search string.columns[...][searchable]andcolumns[...][orderable]– per‑column flags.
The server must build a response JSON object with at least these fields:
draw– same integer received from the request.recordsTotal– total rows in the table before applying any filter.recordsFiltered– total rows after applying the global search (but before paging).data– array of row objects, each containing the values for the columns defined in the client config.
A simple pseudo‑code handler (language‑agnostic) illustrates the contract:
function handleRequest(params):
draw = params.draw
start = int(params.start)
length = int(params.length)
search = params.search['value']
order_col_idx = int(params.order[0]['column'])
order_dir = params.order[0]['dir']
# map DataTables column index to DB column name (never trust raw name)
order_map = {0: 'id', 1: 'first_name', 2: 'last_name', 3: 'position', 4: 'salary'}
order_by = f"{order_map[order_col_idx]} {order_dir}"
# base query
total = db.count('SELECT COUNT(*) FROM employees')
# apply global search if needed
if search:
filtered = db.count("""
SELECT COUNT(*) FROM employees
WHERE id LIKE ? OR first_name LIKE ? OR last_name LIKE ?
OR position LIKE ? OR salary LIKE ?
""", [f'%{search}%']*5)
rows = db.query("""
SELECT id, first_name, last_name, position, salary
FROM employees
WHERE id LIKE ? OR first_name LIKE ? OR last_name LIKE ?
OR position LIKE ? OR salary LIKE ?
ORDER BY ? LIMIT ?, ?
""", [f'%{search}%']*5 + [order_by, start, length])
else:
filtered = total
rows = db.query("""
SELECT id, first_name, last_name, position, salary
FROM employees
ORDER BY ? LIMIT ?, ?
""", [order_by, start, length])
return {
'draw': draw,
'recordsTotal': total,
'recordsFiltered': filtered,
'data': rows
}
Limits and common mistakes
- Confusing
recordsTotalandrecordsFiltered - Using client‑side features that expect the full dataset
- Trusting the client‑supplied column name for sorting
- Excessive requests on keystroke
- Version‑specific defaults
If you set both to the same number (or swap them), the table’s info line shows incorrect counts and the “Next” button may be disabled even when more pages exist. Always compute the unfiltered count for recordsTotal and the filtered count for recordsFiltered.
Plugins such as Buttons with the "copy" or "excel" export, or custom search boxes that scan all rows, will only see the current page when serverSide: true is enabled. To export all rows you must either disable server‑side mode for that action or implement a separate endpoint that returns the complete dataset.
The order[i][column] value is an index; you must map it to a safe column name (as shown in the pseudo‑code). Directly concatenating the index into SQL without validation opens an injection vector.
By default DataTables fires an AJAX request on every keystroke in the global search box. Use the searchDelay option (e.g., searchDelay: 400) to debounce and reduce backend load.
DataTables 2.x changed some styling class names and the default value of searching. Verify the major version you are using and test the layout after upgrading.
Practical verification steps
- Open the browser’s developer tools, go to the Network tab, and filter for XHR requests to
/api/employees. - Trigger a page change, a sort, and a search. Confirm each request includes
draw,start,length,order, andsearch[value]parameters. - Inspect the response: ensure
drawmatches the request,recordsTotalreflects the full table size, andrecordsFilteredchanges appropriately when you type a search term. - Check the table’s info line (e.g., "Showing 11 to 20 of 57 entries") – the first two numbers should shift with paging, while the third number (
recordsFiltered) should stay constant while paging and update only when the search term changes. - Click each column header to sort; verify the backend sorts by the intended field (you can add a log or inspect the SQL generated).
Following this configuration and verification checklist will give you a reliable server‑side DataTables implementation while avoiding the most frequent pitfalls.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.