Une extension d’inventaire exposait une route REST personnalisée listant des références de pièces détachées, consommée par une application mobile développée par une autre équipe. Cette équipe a rapidement signalé que leur bibliothèque cliente, habituée aux routes natives de WordPress comme /wp/v2/posts, ne parvenait pas à savoir combien de pages restaient à charger, faute d’en-têtes de pagination dans la réponse.
Ce comportement n’a rien d’un bug : les routes personnalisées enregistrées via register_rest_route() ne bénéficient d’aucune pagination automatique. Le cœur de WordPress ajoute lui-même les en-têtes X-WP-Total et X-WP-TotalPages sur ses propres contrôleurs, mais une route entièrement maison doit reproduire ce comportement explicitement.
Ce que le client REST attend
La convention établie par le cœur de WordPress, et reprise par la plupart des bibliothèques clientes construites autour de l’API REST, repose sur deux en-têtes de réponse HTTP : X-WP-Total, qui indique le nombre total d’éléments correspondant à la requête, indépendamment de la pagination appliquée, et X-WP-TotalPages, qui indique le nombre total de pages compte tenu du paramètre per_page utilisé.
Construire le contrôleur avec pagination
La route enregistrée pour cette ressource d’inventaire ressemblait à ceci avant correction, un simple retour de tableau sans aucune information de pagination :
register_rest_route( 'inventaire/v1', '/pieces', array(
'methods' => 'GET',
'callback' => 'inventaire_lister_pieces',
'permission_callback' => 'inventaire_verifier_permission',
) );
La version corrigée calcule le total indépendamment de la page demandée, puis construit la réponse avec les en-têtes attendus, en s’appuyant sur la classe WP_REST_Response plutôt qu’un simple tableau brut :

function inventaire_lister_pieces( WP_REST_Request $request ) {
global $wpdb;
$page = max( 1, (int) $request->get_param( 'page' ) ?: 1 );
$per_page = min( 100, max( 1, (int) $request->get_param( 'per_page' ) ?: 20 ) );
$offset = ( $page - 1 ) * $per_page;
$total = (int) $wpdb->get_var(
"SELECT COUNT(*) FROM {$wpdb->prefix}inventaire_pieces"
);
$lignes = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}inventaire_pieces
ORDER BY reference ASC
LIMIT %d OFFSET %d",
$per_page,
$offset
)
);
$total_pages = (int) ceil( $total / $per_page );
$response = new WP_REST_Response( $lignes );
$response->header( 'X-WP-Total', (string) $total );
$response->header( 'X-WP-TotalPages', (string) $total_pages );
return $response;
}
Le calcul du total via un COUNT(*) distinct, exécuté sans les clauses LIMIT et OFFSET, est indispensable : il ne doit jamais dépendre de la page demandée, sous peine de renvoyer un total qui varie selon la position de navigation, ce qui casserait toute logique de pagination côté client.
Ajouter les liens de navigation
Au-delà des deux en-têtes de comptage, le cœur de WordPress ajoute également un en-tête Link standard, au format défini par la RFC 8288, qui fournit directement les URL de la page suivante et de la page précédente, évitant au client de les reconstruire lui-même :
$base_url = rest_url( 'inventaire/v1/pieces' );
if ( $page > 1 ) {
$lien_precedent = add_query_arg( array( 'page' => $page - 1, 'per_page' => $per_page ), $base_url );
$response->link_header( 'prev', $lien_precedent );
}
if ( $page < $total_pages ) {
$lien_suivant = add_query_arg( array( 'page' => $page + 1, 'per_page' => $per_page ), $base_url );
$response->link_header( 'next', $lien_suivant );
}
La méthode link_header(), disponible directement sur WP_REST_Response, gère automatiquement le formatage correct de l’en-tête, sans avoir à concaténer soi-même la syntaxe attendue par la RFC.
Limiter per_page à une valeur raisonnable
Un point de sécurité et de performance souvent oublié : sans limite haute explicite sur per_page, un client mal intentionné ou simplement mal configuré pourrait demander plusieurs milliers d’éléments en une seule requête, générant une charge disproportionnée côté serveur. Le plafond de 100 appliqué dans l’exemple ci-dessus reprend la convention utilisée par les routes natives de WordPress elles-mêmes.
- Toujours calculer
X-WP-Totalvia un comptage indépendant de la pagination appliquée, jamais via la taille du tableau de résultats retournés. - Ajouter systématiquement
X-WP-TotalPages, calculé à partir du total et deper_page, jamais codé en dur. - Plafonner
per_pageà une valeur raisonnable, en cohérence avec les conventions déjà appliquées par le cœur de WordPress sur ses propres routes.
Un test que nous ajoutons désormais systématiquement à toute nouvelle route REST personnalisée : vérifier avec un simple appel
curl -Ique les en-têtes de pagination sont bien présents, avant même de tester le contenu de la réponse elle-même.
En résumé
Une route REST personnalisée n’hérite d’aucune pagination automatique, contrairement aux routes natives de WordPress. Reproduire les en-têtes X-WP-Total, X-WP-TotalPages et, dans l’idéal, Link, ne demande que quelques lignes de code supplémentaires, mais rend la ressource immédiatement compatible avec les attentes des bibliothèques clientes déjà habituées aux conventions du cœur de WordPress.