Développement Web

API REST WordPress : créer un endpoint personnalisé

13 min de lecture

📍 Guide principal : Développement WordPress : plugins, thèmes et blocs. Ce tutoriel ouvre WordPress aux applications externes.

Introduction

Un développeur mobile veut afficher votre annuaire dans une application Android. Il n’a que faire de votre thème ou de vos blocs : il lui faut des données, en JSON, sous une forme propre et stable. C’est exactement ce que fournit l’API REST de WordPress. Chaque installation expose déjà ses contenus sous /wp-json/, et vos types personnalisés y figurent automatiquement si vous les avez bien déclarés. Mais pour offrir une réponse taillée sur mesure — juste le nom, le téléphone, le quartier, sans le bruit du reste — on enregistre sa propre route. À la fin de ce tutoriel, Annuaire Quartier exposera un endpoint annuaire/v1/artisans sécurisé, prêt à nourrir n’importe quelle application.

🎯 Ce que vous allez apprendre

  • Comprendre la structure d’une route REST : espace de noms, version, chemin.
  • Enregistrer une route avec register_rest_route() sur le bon hook.
  • Écrire un callback qui renvoie une réponse JSON normalisée.
  • Protéger une route avec un permission_callback — la faille la plus répandue.
  • Valider les paramètres entrants et consommer l’endpoint depuis JavaScript.

🛠️ Ce que vous allez construire

Une route GET /wp-json/annuaire/v1/artisans qui renvoie la liste des artisans sous une forme épurée, avec un paramètre par_page validé, et une seconde route pour récupérer un artisan précis par son identifiant. On terminera par un appel JavaScript qui consomme l’endpoint, comme le ferait une application.

Prérequis

  • Le type « artisan » et ses métadonnées en place (voir types de contenu et métadonnées).
  • Quelques fiches d’artisans saisies, pour avoir des données à servir.
  • Notions de hooks et de PHP.
  • Test express : si vous savez lire une URL JSON dans votre navigateur, vous comprendrez l’API REST en quelques minutes.
  • ⏱️ Temps estimé : ~45 minutes.

Le modèle mental : routes, espaces de noms, callbacks

Une URL d’API REST se lit comme une adresse postale. Après /wp-json/ vient l’espace de noms qui identifie le fournisseur (annuaire/v1 pour notre extension, version 1), puis le chemin de la ressource (/artisans). À chaque route, on associe trois choses : la ou les méthodes HTTP acceptées (GET pour lire, POST pour créer), un callback qui produit la réponse, et un permission_callback qui décide qui a le droit d’appeler la route. Cette dernière pièce n’est pas optionnelle : c’est elle qui protège vos données. La versionner dans l’espace de noms (v1) vous laissera publier un jour une v2 sans casser les applications qui dépendent de la première.

Étape 1 — Enregistrer une première route

Toutes les routes se déclarent sur le hook rest_api_init. On appelle register_rest_route() avec l’espace de noms, le chemin, et un tableau d’options. Plaçons ce code dans includes/rest-api.php, le fichier que le squelette prévoyait d’inclure.

function aq_register_routes() {
    register_rest_route( 'annuaire/v1', '/artisans', array(
        'methods'             => WP_REST_Server::READABLE,
        'callback'            => 'aq_get_artisans',
        'permission_callback' => '__return_true',
    ) );
}
add_action( 'rest_api_init', 'aq_register_routes' );

WP_REST_Server::READABLE est une constante qui vaut « GET » — on l’utilise plutôt que la chaîne brute par lisibilité. Le callback est la fonction qui construira la réponse, qu’on écrit à l’étape suivante. Le permission_callback vaut ici __return_true : la liste des artisans est publique, donc ouverte à tous. Ce choix est délibéré et assumé ; on verra qu’il ne doit jamais être le réflexe par défaut. Une fois ce code en place, l’URL /wp-json/annuaire/v1/artisans existe, même si elle ne renvoie encore rien d’utile.

Étape 2 — Écrire le callback

Le callback reçoit la requête et renvoie les données. On y fait une requête WP_Query sur le type « artisan », puis on construit un tableau épuré — seulement les champs utiles à une application — qu’on enveloppe dans une réponse REST avec rest_ensure_response().

function aq_get_artisans( $request ) {
    $query = new WP_Query( array(
        'post_type'      => 'aq_artisan',
        'posts_per_page' => 10,
    ) );

    $data = array();
    foreach ( $query->posts as $post ) {
        $data[] = array(
            'id'        => $post->ID,
            'nom'       => get_the_title( $post ),
            'telephone' => get_post_meta( $post->ID, 'aq_telephone', true ),
            'quartier'  => get_post_meta( $post->ID, 'aq_quartier', true ),
        );
    }

    return rest_ensure_response( $data );
}

On ne renvoie que quatre champs choisis, là où la route native /wp/v2/aq_artisan renverrait des dizaines de propriétés. C’est tout l’intérêt d’un endpoint maison : livrer exactement ce dont le client a besoin, ni plus ni moins, ce qui allège la réponse et simplifie le travail côté application. rest_ensure_response() emballe le tableau dans l’objet de réponse standard, qui gère les en-têtes HTTP et le statut. Visitez l’URL dans votre navigateur : vous voyez vos artisans en JSON propre.

Point d’étape/wp-json/annuaire/v1/artisans renvoie un tableau JSON de vos artisans avec nom, téléphone et quartier. Si le tableau est vide, vérifiez que des fiches « artisan » sont bien publiées.

Étape 3 — Valider un paramètre entrant

Notre route renvoie dix artisans en dur. Laissons l’appelant choisir, mais sans lui faire confiance aveuglément. On déclare un argument par_page avec une validation et un nettoyage. WordPress refusera automatiquement les valeurs non conformes avant même d’appeler notre callback.

register_rest_route( 'annuaire/v1', '/artisans', array(
    'methods'             => WP_REST_Server::READABLE,
    'callback'            => 'aq_get_artisans',
    'permission_callback' => '__return_true',
    'args'                => array(
        'par_page' => array(
            'default'           => 10,
            'sanitize_callback' => 'absint',
            'validate_callback' => function( $value ) {
                return $value >= 1 && $value <= 50;
            },
        ),
    ),
) );

absint force une valeur entière positive ; le validate_callback rejette tout ce qui sort de l’intervalle 1 à 50, renvoyant une erreur 400 propre. Dans le callback, on lit le paramètre validé avec $request['par_page'] et on le passe à posts_per_page. Cette discipline — valider et nettoyer chaque entrée — est ce qui sépare une API robuste d’une API qu’on fait planter avec un paramètre exotique. Ne jamais injecter un paramètre brut dans une requête.

Étape 4 — Une route avec identifiant

Pour récupérer un artisan précis, on ajoute une route dont le chemin contient un motif capturant l’identifiant. La syntaxe (?P<id>\d+) capture une suite de chiffres et la nomme id, disponible ensuite dans la requête.

register_rest_route( 'annuaire/v1', '/artisans/(?P<id>\d+)', array(
    'methods'             => WP_REST_Server::READABLE,
    'callback'            => 'aq_get_artisan',
    'permission_callback' => '__return_true',
) );

function aq_get_artisan( $request ) {
    $id   = (int) $request['id'];
    $post = get_post( $id );

    if ( ! $post || 'aq_artisan' !== $post->post_type ) {
        return new WP_Error( 'aq_introuvable', 'Artisan introuvable.', array( 'status' => 404 ) );
    }

    return rest_ensure_response( array(
        'id'        => $post->ID,
        'nom'       => get_the_title( $post ),
        'telephone' => get_post_meta( $post->ID, 'aq_telephone', true ),
    ) );
}

On vérifie que l’identifiant correspond bien à un artisan existant ; sinon, on renvoie un WP_Error avec le statut HTTP 404. WordPress traduit automatiquement ce WP_Error en réponse JSON d’erreur correcte. Gérer le cas « introuvable » proprement évite à l’application cliente de recevoir une réponse ambiguë.

Étape 5 — Le permission_callback, votre rempart

Revenons sur le point le plus sensible. Pour une lecture publique, __return_true est acceptable. Mais dès qu’une route lit des données sensibles ou en écrit, ce réglage devient une faille béante : il ouvre la porte à tout le monde. La règle est simple : une route qui n’est pas explicitement publique exige une vérification de droits réelle. Imaginons une route qui modifie un artisan ; son permission_callback doit refuser les visiteurs sans droits.

register_rest_route( 'annuaire/v1', '/artisans/(?P<id>\d+)', array(
    'methods'             => WP_REST_Server::EDITABLE, // POST/PUT/PATCH
    'callback'            => 'aq_update_artisan',
    'permission_callback' => function() {
        return current_user_can( 'edit_posts' );
    },
) );

Ici, seul un utilisateur autorisé à éditer des contenus passe le contrôle ; les autres reçoivent une erreur 401 ou 403. Renvoyer true sans condition sur une route d’écriture est l’erreur de sécurité la plus fréquente des endpoints maison, et la première que cherchent les outils d’audit. Prenez le réflexe de vous demander, pour chaque route : qui a le droit d’appeler ceci ?

Point d’étape — Vos routes de lecture publiques fonctionnent, votre route par identifiant gère le cas introuvable, et vous savez verrouiller une route d’écriture. La sécurité n’est plus une réflexion d’après-coup.

Étape 6 — Consommer l’endpoint en JavaScript

Voyons enfin le point de vue du client. Une application — ou un script dans une page — lit la route avec fetch(), exactement comme n’importe quelle API JSON. Voici un appel minimal qui récupère trois artisans et affiche leurs noms.

fetch( '/wp-json/annuaire/v1/artisans?par_page=3' )
    .then( ( reponse ) => reponse.json() )
    .then( ( artisans ) => {
        artisans.forEach( ( a ) => {
            console.log( a.nom + ' — ' + a.telephone );
        } );
    } );

La réponse est le tableau épuré que notre callback construit. Côté application mobile, le principe est identique : une requête HTTP, une réponse JSON, un affichage. C’est cette simplicité qui fait la force de l’approche « headless » où WordPress sert de back-end de données à une interface développée séparément. Votre annuaire est désormais consommable bien au-delà de son site d’origine.

Étape 7 — Enrichir une réponse native plutôt qu’en créer une

Créer une route entière n’est pas toujours nécessaire. Parfois, on veut simplement ajouter un champ à la réponse native d’un type — par exemple faire apparaître le métier directement dans /wp/v2/aq_artisan, sans réécrire toute la route. C’est le rôle de register_rest_field(), qui greffe un champ calculé sur une réponse existante.

function aq_register_rest_fields() {
    register_rest_field( 'aq_artisan', 'metiers', array(
        'get_callback' => function( $objet ) {
            return wp_get_post_terms( $objet['id'], 'aq_metier', array( 'fields' => 'names' ) );
        },
        'schema' => array(
            'description' => 'Métiers de l\'artisan',
            'type'        => 'array',
        ),
    ) );
}
add_action( 'rest_api_init', 'aq_register_rest_fields' );

Le get_callback reçoit la représentation de l’objet et renvoie la valeur du champ — ici, la liste des métiers. Le bloc schema documente le champ, ce qui le rend visible dans la description auto-générée de l’API. Cette approche est plus légère qu’une route complète quand le besoin se résume à « il manque juste cette donnée dans la réponse standard ». Savoir choisir entre une route maison et un simple champ ajouté est une marque de maturité dans la conception d’API.

🐞 Pièges fréquents

Symptôme / erreur Cause probable Correctif
No route was found (404) Route déclarée hors de rest_api_init Toujours enregistrer sur ce hook
permission_callback is required Argument oublié depuis WordPress 5.5 Le fournir explicitement, même pour une route publique
Route d’écriture accessible à tous permission_callback renvoyant true Vérifier les droits réels avec current_user_can()
Paramètre qui fait planter la requête Valeur non validée injectée directement Déclarer sanitize_callback et validate_callback
Réponse 403 sur tout l’API REST Règle de sécurité serveur trop stricte Vérifier la configuration de l’hébergeur ou du pare-feu applicatif

✅ Récapitulatif

Vous avez enregistré des routes REST sur mesure avec register_rest_route(), écrit des callbacks renvoyant des réponses JSON épurées via rest_ensure_response(), validé et nettoyé les paramètres entrants, géré le cas « introuvable » avec WP_Error, et — surtout — appris à protéger chaque route avec un permission_callback réel. Vous avez enfin consommé l’endpoint en JavaScript. Annuaire Quartier est maintenant un fournisseur de données ouvert au monde, dans les limites que vous fixez.

🧾 Aide-mémoire

Élément Rôle
register_rest_route( $ns, $chemin, $args ) Déclare une route (sur rest_api_init)
WP_REST_Server::READABLE / ::EDITABLE Méthodes GET / écriture
permission_callback Contrôle d’accès — jamais optionnel
rest_ensure_response() Emballe les données en réponse REST
sanitize_callback / validate_callback Nettoyage / validation des paramètres
WP_Error( $code, $msg, ['status'=>404] ) Renvoie une erreur HTTP propre

💪 À vous de jouer

Ajoutez un champ metier à la réponse de la liste, en récupérant les termes de la taxonomie de chaque artisan. Indice : wp_get_post_terms() renvoie les termes attachés à un contenu.

Voir une solution
$metiers = wp_get_post_terms( $post->ID, 'aq_metier', array( 'fields' => 'names' ) );
$data[] = array(
    'id'      => $post->ID,
    'nom'     => get_the_title( $post ),
    'metiers' => $metiers, // tableau de noms de métiers
);

L’option 'fields' => 'names' renvoie directement les noms, plus pratiques à consommer qu’des objets complets côté application.

Tutoriels de la série

Pour aller plus loin

FAQ

Pourquoi créer une route si mon type est déjà dans l’API native ?

La route native /wp/v2/aq_artisan convient pour beaucoup de cas. Une route maison se justifie quand vous voulez une forme de réponse différente — épurée, agrégée, ou combinant plusieurs sources — ou une logique d’accès particulière. C’est un choix de conception, pas une obligation.

Le permission_callback est-il vraiment obligatoire ?

Depuis WordPress 5.5, oui : une route sans permission_callback déclenche un avertissement et est considérée comme mal configurée. Pour une route publique, on l’indique explicitement avec __return_true, ce qui rend le choix d’ouverture visible et assumé.

Comment authentifier une application externe ?

Pour un accès en lecture publique, aucune authentification n’est nécessaire. Pour des routes protégées, WordPress propose les mots de passe d’application, qui permettent à un client externe de s’authentifier sans le mot de passe principal du compte. Le permission_callback vérifie alors les droits de l’utilisateur authentifié.

Dois-je versionner mon espace de noms ?

C’est une bonne pratique. En nommant annuaire/v1, vous pourrez introduire une annuaire/v2 au format différent sans casser les applications qui dépendent de la première. Les clients migrent à leur rythme.

Comment limiter le nombre de requêtes sur mon API ?

WordPress n’impose pas de limitation de débit nativement. Sur un site exposé, on s’appuie sur le pare-feu applicatif de l’hébergeur ou un cache de page pour les réponses publiques. Pour des besoins fins, une logique de limitation peut s’ajouter dans le permission_callback.

Partager