Introduction
L’API REST de WordPress est devenue un composant essentiel pour créer des applications modernes, des interfaces découplées et des intégrations tierces. En 2025, la sécurité des endpoints personnalisés est plus critique que jamais, avec une attention particulière portée à l’authentification, la validation des données et la limitation du débit. Ce guide complet vous accompagnera dans la création d’endpoints REST API sécurisés et performants.
Comprendre l’architecture de l’API REST WordPress
Anatomie d’une requête REST
L’API REST WordPress suit les conventions RESTful standards :
GET: Récupérer des donnéesPOST: Créer de nouvelles donnéesPUT/PATCH: Mettre à jour des données existantesDELETE: Supprimer des données
Les endpoints WordPress suivent cette structure :
https://example.com/wp-json/namespace/v1/resource
Structure de base d’un endpoint
Voici les composants essentiels d’un endpoint REST :
namespace, '/items', array(
'methods' => WP_REST_Server::READABLE, // GET
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
) );
// Route POST
register_rest_route( $this->namespace, '/items', array(
'methods' => WP_REST_Server::CREATABLE, // POST
'callback' => array( $this, 'create_item' ),
'permission_callback' => array( $this, 'create_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema(),
) );
// Route pour un item spécifique (GET, PUT, DELETE)
register_rest_route( $this->namespace, '/items/(?Pd+)', array(
array(
'methods' => WP_REST_Server::READABLE, // GET
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
'id' => array(
'description' => **( 'Identifiant unique de l'item.', 'mon-plugin' ),
'type' => 'integer',
'required' => true,
),
),
),
array(
'methods' => WP_REST_Server::EDITABLE, // PUT/PATCH
'callback' => array( $this, 'update_item' ),
'permission_callback' => array( $this, 'update_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( false ),
),
array(
'methods' => WP_REST_Server::DELETABLE, // DELETE
'callback' => array( $this, 'delete_item' ),
'permission_callback' => array( $this, 'delete_item_permissions_check' ),
),
'schema' => array( $this, 'get_public_item_schema' ),
) );
}
/**
* Récupérer une collection d'items
*/
public function get_items( $request ) {
$items = array();
$data = array();
// Récupérer les paramètres de pagination
$per_page = $request->get_param( 'per_page' );
$page = $request->get_param( 'page' );
$search = $request->get_param( 'search' );
// Requête à la base de données
global $wpdb;
$table_name = $wpdb->prefix . 'mon_plugin_items';
$offset = ( $page - 1 ) * $per_page;
$where = '';
if ( ! empty( $search ) ) {
$where = $wpdb->prepare( " WHERE title LIKE %s", '%' . $wpdb->esc_like( $search ) . '%' );
}
$items = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM $table_name $where ORDER BY created_at DESC LIMIT %d OFFSET %d",
$per_page,
$offset
)
);
// Préparer les données pour la réponse
foreach ( $items as $item ) {
$itemdata = $this->prepare_item_for_response( $item, $request );
$data[] = $this->prepare_response_for_collection( $itemdata );
}
// Compter le total pour les en-têtes de pagination
$total_items = $wpdb->get_var( "SELECT COUNT(*) FROM $table_name $where" );
$max_pages = ceil( $total_items / $per_page );
// Créer la réponse avec en-têtes
$response = rest_ensure_response( $data );
$response->header( 'X-WP-Total', (int) $total_items );
$response->header( 'X-WP-TotalPages', (int) $max_pages );
return $response;
}
/**
* Récupérer un seul item
*/
public function get_item( $request ) {
$id = $request->get_param( 'id' );
global $wpdb;
$table_name = $wpdb->prefix . 'mon_plugin_items';
$item = $wpdb->get_row(
$wpdb->prepare( "SELECT * FROM $table_name WHERE id = %d", $id )
);
if ( empty( $item ) ) {
return new WP_Error(
'rest_item_not_found',
**( 'Item non trouvé.', 'mon-plugin' ),
array( 'status' => 404 )
);
}
$data = $this->prepare_item_for_response( $item, $request );
return rest_ensure_response( $data );
}
/**
* Créer un nouvel item
*/
public function create_item( $request ) {
global $wpdb;
$table_name = $wpdb->prefix . 'mon_plugin_items';
// Récupérer et valider les données
$title = sanitize_text_field( $request->get_param( 'title' ) );
$description = wp_kses_post( $request->get_param( 'description' ) );
$status = sanitize_text_field( $request->get_param( 'status' ) );
$user_id = get_current_user_id();
// Insérer dans la base de données
$inserted = $wpdb->insert(
$table_name,
array(
'title' => $title,
'description' => $description,
'status' => $status,
'user_id' => $user_id,
'created_at' => current_time( 'mysql' ),
),
array( '%s', '%s', '%s', '%d', '%s' )
);
if ( ! $inserted ) {
return new WP_Error(
'rest_item_create_failed',
**( 'Impossible de créer l'item.', 'mon-plugin' ),
array( 'status' => 500 )
);
}
$item_id = $wpdb->insert_id;
// Récupérer l'item créé
$item = $wpdb->get_row(
$wpdb->prepare( "SELECT * FROM $table_name WHERE id = %d", $item_id )
);
$data = $this->prepare_item_for_response( $item, $request );
$response = rest_ensure_response( $data );
$response->set_status( 201 );
$response->header( 'Location', rest_url( sprintf( '%s/items/%d', $this->namespace, $item_id ) ) );
return $response;
}
/**
* Mettre à jour un item
*/
public function update_item( $request ) {
$id = $request->get_param( 'id' );
global $wpdb;
$table_name = $wpdb->prefix . 'mon_plugin_items';
// Vérifier que l'item existe
$item = $wpdb->get_row(
$wpdb->prepare( "SELECT * FROM $table_name WHERE id = %d", $id )
);
if ( empty( $item ) ) {
return new WP_Error(
'rest_item_not_found',
**( 'Item non trouvé.', 'mon-plugin' ),
array( 'status' => 404 )
);
}
// Préparer les données à mettre à jour
$update_data = array();
$update_format = array();
if ( $request->has_param( 'title' ) ) {
$update_data['title'] = sanitize_text_field( $request->get_param( 'title' ) );
$update_format[] = '%s';
}
if ( $request->has_param( 'description' ) ) {
$update_data['description'] = wp_kses_post( $request->get_param( 'description' ) );
$update_format[] = '%s';
}
if ( $request->has_param( 'status' ) ) {
$update_data['status'] = sanitize_text_field( $request->get_param( 'status' ) );
$update_format[] = '%s';
}
$update_data['updated_at'] = current_time( 'mysql' );
$update_format[] = '%s';
// Mettre à jour dans la base de données
$updated = $wpdb->update(
$table_name,
$update_data,
array( 'id' => $id ),
$update_format,
array( '%d' )
);
if ( false === $updated ) {
return new WP_Error(
'rest_item_update_failed',
**( 'Impossible de mettre à jour l'item.', 'mon-plugin' ),
array( 'status' => 500 )
);
}
// Récupérer l'item mis à jour
$item = $wpdb->get_row(
$wpdb->prepare( "SELECT * FROM $table_name WHERE id = %d", $id )
);
$data = $this->prepare_item_for_response( $item, $request );
return rest_ensure_response( $data );
}
/**
* Supprimer un item
*/
public function delete_item( $request ) {
$id = $request->get_param( 'id' );
global $wpdb;
$table_name = $wpdb->prefix . 'mon_plugin_items';
// Vérifier que l'item existe
$item = $wpdb->get_row(
$wpdb->prepare( "SELECT * FROM $table_name WHERE id = %d", $id )
);
if ( empty( $item ) ) {
return new WP_Error(
'rest_item_not_found',
**( 'Item non trouvé.', 'mon-plugin' ),
array( 'status' => 404 )
);
}
// Préparer la réponse avant suppression
$previous = $this->prepare_item_for_response( $item, $request );
// Supprimer de la base de données
$deleted = $wpdb->delete(
$table_name,
array( 'id' => $id ),
array( '%d' )
);
if ( ! $deleted ) {
return new WP_Error(
'rest_item_delete_failed',
**( 'Impossible de supprimer l'item.', 'mon-plugin' ),
array( 'status' => 500 )
);
}
$response = new WP_REST_Response();
$response->set_data( array(
'deleted' => true,
'previous' => $previous->get_data(),
) );
return $response;
}
/**
* Préparer un item pour la réponse
*/
public function prepare_item_for_response( $item, $request ) {
$data = array(
'id' => (int) $item->id,
'title' => $item->title,
'description' => $item->description,
'status' => $item->status,
'user_id' => (int) $item->user_id,
'created_at' => mysql2date( 'c', $item->created_at ),
'updated_at' => $item->updated_at ? mysql2date( 'c', $item->updated_at ) : null,
);
// Ajouter des liens
$data['*links'] = array(
'self' => array(
'href' => rest_url( sprintf( '%s/items/%d', $this->namespace, $item->id ) ),
),
'collection' => array(
'href' => rest_url( sprintf( '%s/items', $this->namespace ) ),
),
);
if ( ! empty( $item->user_id ) ) {
$data['*links']['author'] = array(
'href' => rest_url( sprintf( 'wp/v2/users/%d', $item->user_id ) ),
'embeddable' => true,
);
}
return rest_ensure_response( $data );
}
/**
* Préparer la réponse pour une collection
*/
public function prepare_response_for_collection( $response ) {
if ( ! ( $response instanceof WP_REST_Response ) ) {
return $response;
}
$data = (array) $response->get_data();
$server = rest_get_server();
$links = $server::get_compact_response_links( $response );
if ( ! empty( $links ) ) {
$data['*links'] = $links;
}
return $data;
}
/**
* Paramètres de collection (pagination, recherche)
*/
public function get_collection_params() {
return array(
'page' => array(
'description' => **( 'Page actuelle de la collection.', 'mon-plugin' ),
'type' => 'integer',
'default' => 1,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
'minimum' => 1,
),
'per_page' => array(
'description' => **( 'Nombre maximum d'items retournés.', 'mon-plugin' ),
'type' => 'integer',
'default' => 10,
'minimum' => 1,
'maximum' => 100,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
),
'search' => array(
'description' => **( 'Rechercher dans les items.', 'mon-plugin' ),
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'validate_callback' => 'rest_validate_request_arg',
),
);
}
/**
* Arguments pour créer/modifier un item
*/
public function get_endpoint_args_for_item_schema( $method_create = true ) {
return array(
'title' => array(
'description' => **( 'Titre de l'item.', 'mon-plugin' ),
'type' => 'string',
'required' => $method_create,
'sanitize_callback' => 'sanitize_text_field',
'validate_callback' => 'rest_validate_request_arg',
),
'description' => array(
'description' => **( 'Description de l'item.', 'mon-plugin' ),
'type' => 'string',
'sanitize_callback' => 'wp_kses_post',
'validate_callback' => 'rest_validate_request_arg',
),
'status' => array(
'description' => **( 'Statut de l'item.', 'mon-plugin' ),
'type' => 'string',
'enum' => array( 'draft', 'published', 'archived' ),
'default' => 'draft',
'sanitize_callback' => 'sanitize_text_field',
'validate_callback' => 'rest_validate_request_arg',
),
);
}
/**
* Schéma de l'item
*/
public function get_public_item_schema() {
if ( $this->schema ) {
return $this->add_additional_fields_schema( $this->schema );
}
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'item',
'type' => 'object',
'properties' => array(
'id' => array(
'description' => **( 'Identifiant unique de l'item.', 'mon-plugin' ),
'type' => 'integer',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
'title' => array(
'description' => **( 'Titre de l'item.', 'mon-plugin' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
),
'description' => array(
'description' => **( 'Description de l'item.', 'mon-plugin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'status' => array(
'description' => **( 'Statut de l'item.', 'mon-plugin' ),
'type' => 'string',
'enum' => array( 'draft', 'published', 'archived' ),
'context' => array( 'view', 'edit' ),
),
'user_id' => array(
'description' => **( 'ID de l'auteur.', 'mon-plugin' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'created_at' => array(
'description' => **( 'Date de création (ISO8601).', 'mon-plugin' ),
'type' => 'string',
'format' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'updated_at' => array(
'description' => **( 'Date de dernière modification (ISO8601).', 'mon-plugin' ),
'type' => array( 'string', 'null' ),
'format' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
);
$this->schema = $schema;
return $this->add_additional_fields_schema( $this->schema );
}
/**
* Vérifications de permissions - Récupérer items
*/
public function get_items_permissions_check( $request ) {
// Accessible publiquement
return true;
}
/**
* Vérifications de permissions - Récupérer un item
*/
public function get_item_permissions_check( $request ) {
// Accessible publiquement
return true;
}
/**
* Vérifications de permissions - Créer un item
*/
public function create_item_permissions_check( $request ) {
// Seuls les utilisateurs connectés peuvent créer
if ( ! is_user_logged_in() ) {
return new WP_Error(
'rest_forbidden',
**( 'Vous devez être connecté pour créer un item.', 'mon-plugin' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Vérifications de permissions - Mettre à jour un item
*/
public function update_item_permissions_check( $request ) {
if ( ! is_user_logged_in() ) {
return new WP_Error(
'rest_forbidden',
**( 'Vous devez être connecté pour modifier un item.', 'mon-plugin' ),
array( 'status' => rest_authorization_required_code() )
);
}
$id = $request->get_param( 'id' );
global $wpdb;
$table_name = $wpdb->prefix . 'mon_plugin_items';
$item = $wpdb->get_row(
$wpdb->prepare( "SELECT * FROM $table_name WHERE id = %d", $id )
);
if ( empty( $item ) ) {
return new WP_Error(
'rest_item_not_found',
**( 'Item non trouvé.', 'mon-plugin' ),
array( 'status' => 404 )
);
}
// Vérifier que l'utilisateur est propriétaire ou admin
$current_user_id = get_current_user_id();
if ( (int) $item->user_id !== $current_user_id && ! current_user_can( 'manage_options' ) ) {
return new WP_Error(
'rest_forbidden',
**( 'Vous n'avez pas la permission de modifier cet item.', 'mon-plugin' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Vérifications de permissions - Supprimer un item
*/
public function delete_item_permissions_check( $request ) {
return $this->update_item_permissions_check( $request );
}
}
// Initialiser l'endpoint
new EndpointExample();
Authentification sécurisée
Méthodes d’authentification en 2025
WordPress offre plusieurs méthodes d’authentification :
1. Authentification par cookie (sessions WordPress)
Utilisée automatiquement pour les requêtes depuis le site WordPress :
get_header( 'X-WP-Nonce' );
if ( ! wp_verify_nonce( $nonce, 'wp_rest' ) ) {
return new WP_Error(
'rest_cookie_invalid_nonce',
**( 'Nonce invalide.', 'mon-plugin' ),
array( 'status' => 403 )
);
}
return true;
}
2. Authentification par clé API personnalisée
get_api_key_from_request();
if ( empty( $api_key ) ) {
return $user_id;
}
// Valider la clé API
$user_id = $this->validate_api_key( $api_key );
return $user_id;
}
/**
* Récupérer la clé API depuis la requête
*/
private function get_api_key_from_request() {
// Vérifier dans les en-têtes
$api_key = null;
if ( isset( $*SERVER['HTTP_X_API_KEY'] ) ) {
$api_key = sanitize_text_field( wp_unslash( $*SERVER['HTTP_X_API_KEY'] ) );
} elseif ( isset( $*GET['api_key'] ) ) {
// Fallback pour query parameter (moins sécurisé)
$api_key = sanitize_text_field( wp_unslash( $*GET['api_key'] ) );
}
return $api_key;
}
/**
* Valider la clé API et retourner l'ID utilisateur
*/
private function validate_api_key( $api_key ) {
global $wpdb;
// Hasher la clé pour comparaison
$api_key_hash = hash( 'sha256', $api_key );
$user_id = $wpdb->get_var(
$wpdb->prepare(
"SELECT user_id FROM {$wpdb->usermeta}
WHERE meta_key = 'mon_plugin_api_key_hash'
AND meta_value = %s
LIMIT 1",
$api_key_hash
)
);
if ( $user_id ) {
// Logger l'utilisation de la clé
$this->log_api_key_usage( $user_id, $api_key_hash );
return (int) $user_id;
}
return 0;
}
/**
* Logger l'utilisation de la clé API
*/
private function log_api_key_usage( $user_id, $api_key_hash ) {
global $wpdb;
$wpdb->insert(
$wpdb->prefix . 'mon_plugin_api_logs',
array(
'user_id' => $user_id,
'api_key_hash' => $api_key_hash,
'endpoint' => sanitize_text_field( $*SERVER['REQUEST_URI'] ),
'ip_address' => $this->get_client_ip(),
'user_agent' => sanitize_text_field( $*SERVER['HTTP_USER_AGENT'] ),
'timestamp' => current_time( 'mysql' ),
),
array( '%d', '%s', '%s', '%s', '%s', '%s' )
);
}
/**
* Récupérer l'IP du client
*/
private function get_client_ip() {
$ip = '';
if ( isset( $*SERVER['HTTP_CF_CONNECTING_IP'] ) ) {
// Cloudflare
$ip = sanitize_text_field( wp_unslash( $*SERVER['HTTP_CF_CONNECTING_IP'] ) );
} elseif ( isset( $*SERVER['HTTP_X_FORWARDED_FOR'] ) ) {
$ip = sanitize_text_field( wp_unslash( $*SERVER['HTTP_X_FORWARDED_FOR'] ) );
} elseif ( isset( $*SERVER['REMOTE_ADDR'] ) ) {
$ip = sanitize_text_field( wp_unslash( $*SERVER['REMOTE_ADDR'] ) );
}
return $ip;
}
/**
* Gérer les erreurs d'authentification
*/
public function rest_authentication_errors( $error ) {
// Ne pas vérifier sur les routes publiques
if ( ! empty( $error ) ) {
return $error;
}
// Vérifier si l'authentification par API key est requise
if ( ! is_user_logged_in() ) {
$api_key = $this->get_api_key_from_request();
if ( $api_key && ! $this->validate_api_key( $api_key ) ) {
return new WP_Error(
'rest_api_key_invalid',
**( 'Clé API invalide.', 'mon-plugin' ),
array( 'status' => 401 )
);
}
}
return $error;
}
/**
* Générer une nouvelle clé API pour un utilisateur
*/
public static function generate_api_key( $user_id ) {
// Générer une clé aléatoire sécurisée
$api_key = bin2hex( random_bytes( 32 ) );
// Hasher et stocker
$api_key_hash = hash( 'sha256', $api_key );
update_user_meta( $user_id, 'mon_plugin_api_key_hash', $api_key_hash );
update_user_meta( $user_id, 'mon_plugin_api_key_created', current_time( 'mysql' ) );
// Retourner la clé non hashée (à afficher une seule fois)
return $api_key;
}
/**
* Révoquer la clé API d'un utilisateur
*/
public static function revoke_api_key( $user_id ) {
delete_user_meta( $user_id, 'mon_plugin_api_key_hash' );
delete_user_meta( $user_id, 'mon_plugin_api_key_created' );
}
}
new APIKeyAuth();
Limitation du débit (Rate Limiting)
Implémentez un rate limiting pour prévenir les abus :
get_route();
if ( strpos( $route, '/mon-plugin/' ) !== 0 ) {
return $result;
}
$identifier = $this->get_identifier();
$current_count = $this->get_request_count( $identifier );
if ( $current_count >= $this->max_requests ) {
return new WP_Error(
'rest_rate_limit_exceeded',
sprintf(
**( 'Limite de débit dépassée. Maximum %d requêtes par heure.', 'mon-plugin' ),
$this->max_requests
),
array(
'status' => 429,
'headers' => array(
'X-RateLimit-Limit' => $this->max_requests,
'X-RateLimit-Remaining' => 0,
'X-RateLimit-Reset' => $this->get_reset_time( $identifier ),
),
)
);
}
// Incrémenter le compteur
$this->increment_request_count( $identifier );
// Ajouter les en-têtes de rate limit
add_filter( 'rest_post_dispatch', function( $response ) use ( $current_count ) {
$response->header( 'X-RateLimit-Limit', $this->max_requests );
$response->header( 'X-RateLimit-Remaining', max( 0, $this->max_requests - $current_count - 1 ) );
return $response;
} );
return $result;
}
/**
* Obtenir un identifiant unique (IP ou User ID)
*/
private function get_identifier() {
$user_id = get_current_user_id();
if ( $user_id ) {
return 'user*' . $user_id;
}
// Utiliser l'IP pour les utilisateurs non connectés
$ip = $this->get_client_ip();
return 'ip*' . md5( $ip );
}
/**
* Obtenir le nombre de requêtes
*/
private function get_request_count( $identifier ) {
$transient_key = 'mon_plugin_rate_limit*' . $identifier;
$count = get_transient( $transient_key );
return $count ? (int) $count : 0;
}
/**
* Incrémenter le compteur de requêtes
*/
private function increment_request_count( $identifier ) {
$transient_key = 'mon_plugin_rate_limit*' . $identifier;
$count = $this->get_request_count( $identifier );
set_transient( $transient_key, $count + 1, $this->time_window );
}
/**
* Obtenir le timestamp de réinitialisation
*/
private function get_reset_time( $identifier ) {
$transient_key = 'mon_plugin_rate_limit*' . $identifier;
$timeout = get_option( '*transient_timeout*' . $transient_key );
return $timeout ? (int) $timeout : time() + $this->time_window;
}
/**
* Récupérer l'IP du client
*/
private function get_client_ip() {
$ip = '';
if ( isset( $*SERVER['HTTP_CF_CONNECTING_IP'] ) ) {
$ip = sanitize_text_field( wp_unslash( $*SERVER['HTTP_CF_CONNECTING_IP'] ) );
} elseif ( isset( $*SERVER['HTTP_X_FORWARDED_FOR'] ) ) {
$ip = sanitize_text_field( wp_unslash( $*SERVER['HTTP_X_FORWARDED_FOR'] ) );
} elseif ( isset( $*SERVER['REMOTE_ADDR'] ) ) {
$ip = sanitize_text_field( wp_unslash( $*SERVER['REMOTE_ADDR'] ) );
}
return $ip;
}
}
new RateLimiter();
Validation et sanitization avancées
Validateurs personnalisés
400 )
);
}
// Vérifier le domaine
$allowed_domains = array( 'example.com', 'test.com' );
$email_parts = explode( '@', $value );
$domain = end( $email_parts );
if ( ! in_array( $domain, $allowed_domains, true ) ) {
return new WP_Error(
'rest_invalid_email_domain',
sprintf(
**( 'Le domaine email doit être l'un des suivants : %s', 'mon-plugin' ),
implode( ', ', $allowed_domains )
),
array( 'status' => 400 )
);
}
return true;
}
/**
* Valider un numéro de téléphone
*/
public static function validate_phone( $value, $request, $param ) {
// Format : +33612345678 ou 0612345678
$pattern = '/^(+33|0)1-9$/';
if ( ! preg_match( $pattern, $value ) ) {
return new WP_Error(
'rest_invalid_phone',
**( 'Le numéro de téléphone n'est pas valide.', 'mon-plugin' ),
array( 'status' => 400 )
);
}
return true;
}
/**
* Valider une URL avec domaines autorisés
*/
public static function validate_url_domain( $value, $request, $param ) {
if ( ! filter_var( $value, FILTER_VALIDATE_URL ) ) {
return new WP_Error(
'rest_invalid_url',
**( 'L'URL n'est pas valide.', 'mon-plugin' ),
array( 'status' => 400 )
);
}
$allowed_domains = array( 'example.com', 'cdn.example.com' );
$parsed_url = wp_parse_url( $value );
$host = $parsed_url['host'] ?? '';
if ( ! in_array( $host, $allowed_domains, true ) ) {
return new WP_Error(
'rest_invalid_url_domain',
**( 'Le domaine de l'URL n'est pas autorisé.', 'mon-plugin' ),
array( 'status' => 400 )
);
}
return true;
}
/**
* Valider une date au format ISO 8601
*/
public static function validate_iso8601_date( $value, $request, $param ) {
$date = DateTime::createFromFormat( DateTime::ISO8601, $value );
if ( ! $date || $date->format( DateTime::ISO8601 ) !== $value ) {
return new WP_Error(
'rest_invalid_date',
**( 'La date doit être au format ISO 8601.', 'mon-plugin' ),
array( 'status' => 400 )
);
}
return true;
}
/**
* Valider un JSON
*/
public static function validate_json( $value, $request, $param ) {
json_decode( $value );
if ( json_last_error() !== JSON_ERROR_NONE ) {
return new WP_Error(
'rest_invalid_json',
sprintf(
**( 'JSON invalide : %s', 'mon-plugin' ),
json_last_error_msg()
),
array( 'status' => 400 )
);
}
return true;
}
/**
* Sanitizer pour JSON
*/
public static function sanitize_json( $value ) {
$decoded = json_decode( $value, true );
if ( json_last_error() !== JSON_ERROR_NONE ) {
return '';
}
// Re-encoder pour garantir un JSON propre
return wp_json_encode( $decoded );
}
}
Utilisation depuis JavaScript
Exemple avec Fetch API
/**
* Client JavaScript pour l'API REST personnalisée
*/
class MonPluginAPIClient {
constructor(baseUrl = '/wp-json/mon-plugin/v1') {
this.baseUrl = baseUrl;
this.nonce = wpApiSettings?.nonce || '';
}
/**
* Effectuer une requête HTTP
*/
async request(endpoint, options = {}) {
const url = ${this.baseUrl}${endpoint};
const defaultOptions = {
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': this.nonce,
},
};
const mergedOptions = {
...defaultOptions,
...options,
headers: {
...defaultOptions.headers,
...options.headers,
},
};
try {
const response = await fetch(url, mergedOptions);
// Vérifier les en-têtes de rate limiting
const rateLimit = {
limit: response.headers.get('X-RateLimit-Limit'),
remaining: response.headers.get('X-RateLimit-Remaining'),
reset: response.headers.get('X-RateLimit-Reset'),
};
if (rateLimit.limit) {
console.log('Rate Limit:', rateLimit);
}
const data = await response.json();
if (!response.ok) {
throw new Error(data.message || 'Erreur API');
}
return {
success: true,
data: data,
status: response.status,
headers: response.headers,
};
} catch (error) {
console.error('Erreur API:', error);
return {
success: false,
error: error.message,
};
}
}
/**
* GET - Récupérer tous les items
*/
async getItems(params = {}) {
const queryString = new URLSearchParams(params).toString();
const endpoint = queryString ? /items?${queryString} : '/items';
return this.request(endpoint, {
method: 'GET',
});
}
/**
* GET - Récupérer un item
*/
async getItem(id) {
return this.request(/items/${id}, {
method: 'GET',
});
}
/**
* POST - Créer un item
*/
async createItem(data) {
return this.request('/items', {
method: 'POST',
body: JSON.stringify(data),
});
}
/**
* PUT - Mettre à jour un item
*/
async updateItem(id, data) {
return this.request(/items/${id}, {
method: 'PUT',
body: JSON.stringify(data),
});
}
/**
* DELETE - Supprimer un item
*/
async deleteItem(id) {
return this.request(/items/${id}, {
method: 'DELETE',
});
}
}
// Utilisation
(async () => {
const api = new MonPluginAPIClient();
// Récupérer tous les items avec pagination
const items = await api.getItems({ page: 1, per_page: 10, search: 'test' });
console.log('Items:', items);
// Créer un nouvel item
const newItem = await api.createItem({
title: 'Mon nouvel item',
description: 'Description de l'item',
status: 'published',
});
console.log('Item créé:', newItem);
// Mettre à jour un item
if (newItem.success) {
const updated = await api.updateItem(newItem.data.id, {
title: 'Titre modifié',
});
console.log('Item mis à jour:', updated);
}
// Supprimer un item
if (newItem.success) {
const deleted = await api.deleteItem(newItem.data.id);
console.log('Item supprimé:', deleted);
}
})();
Tests et debugging
Tester avec WP-CLI
# Lister toutes les routes
wp rest list
# Tester un endpoint GET
wp rest get /mon-plugin/v1/items
# Tester un endpoint POST
wp rest post /mon-plugin/v1/items --title="Test Item" --status="published"
# Tester avec un utilisateur spécifique
wp rest get /mon-plugin/v1/items --user=admin
Logger les requêtes pour debugging
get_route();
if ( strpos( $route, '/mon-plugin/' ) !== 0 ) {
return $result;
}
error_log( sprintf(
'[API Request] %s %s | Params: %s',
$request->get_method(),
$route,
wp_json_encode( $request->get_params() )
) );
return $result;
}
/**
* Logger la réponse
*/
public function log_response( $response, $server, $request ) {
if ( ! WP_DEBUG || ! WP_DEBUG_LOG ) {
return $response;
}
$route = $request->get_route();
if ( strpos( $route, '/mon-plugin/' ) !== 0 ) {
return $response;
}
$status = $response->get_status();
$data = $response->get_data();
error_log( sprintf(
'[API Response] %s %s | Status: %d | Data: %s',
$request->get_method(),
$route,
$status,
wp_json_encode( $data )
) );
return $response;
}
}
if ( WP_DEBUG ) {
new APILogger();
}
Erreurs courantes à éviter
1. Ne pas valider les permissions
MAUVAIS :
'permission_callback' => '**return_true'
BON :
'permission_callback' => array( $this, 'check_permissions' )
2. Oublier la sanitization
MAUVAIS :
$title = $*POST['title'];
BON :
$title = sanitize_text_field( $request->get_param( 'title' ) );
3. Ne pas gérer les erreurs
MAUVAIS :
return $data;
BON :
if ( ! $data ) {
return new WP_Error( 'rest_not_found', **( 'Données non trouvées.' ), array( 'status' => 404 ) );
}
return rest_ensure_response( $data );
Conclusion
La création d’endpoints REST API personnalisés en 2025 nécessite une attention particulière à la sécurité, avec l’implémentation systématique de l’authentification, de la validation des données, du rate limiting et du logging. En suivant ces pratiques, vous créerez des APIs robustes et sécurisées.
Points clés :
Ressources supplémentaires
Mots-clés:* WordPress REST API, endpoints personnalisés WordPress, API REST sécurité, authentification WordPress API, rate limiting WordPress, validation REST API, WordPress API 2025, WP REST API custom, WordPress API développement