REST API WordPress: registrare endpoint custom con register_rest_route e permission_callback

La REST API di WordPress è completa ma ogni endpoint custom mal scritto può pesare 200KB di JSON e bypassare la page cache. Come registrare endpoint performanti, validare l'input con Zod-like schemas, e perché l'autenticazione application passwords non basta per tutto.
La REST API di WordPress esiste dal 2015 ma la usiamo davvero solo quando ci serve collegare qualcosa di esterno — un'app, un CRM, un frontend headless, un servizio di automazione. Il problema non è che manchi, è che la maggior parte degli endpoint custom che vediamo in produzione sono scritti male. Restituiscono troppo, non validano l'input, non gestiscono la cache, e si fidano dell'autenticazione di default. Un endpoint che restituisce 50 post con immagini e meta può pesare 200KB di JSON — se il frontend lo chiama a ogni page load, il risparmio di tempo rispetto a un HTML renderizzato si mangia tutto.
Il primo errore classico è registrare l'endpoint senza pensare a cosa restituisce. WordPress di default espone tutto l'oggetto post — titolo, contenuto, excerpt, data, modificato, link, autore, featured media, e una sfilza di meta che il 90% delle volte non servono. Se il tuo frontend vuole solo titolo, slug e copertina, restituisci solo quelli. Il payload passa da 4KB a 400 byte per post, e su 50 post la differenza è tra una risposta istantanea e una che fa timeout sul mobile.
Registrare un endpoint minimale e performante
La registrazione avviene con register_rest_route nel file functions.php del tema child o (meglio) in un MU plugin. L'endpoint deve dichiarare metodo, callback, permessi e argomenti. Il permission_callback non è opzionale — se lo ometti, WordPress restituisce un errore di deprecation e in futuro rifiuterà la registrazione. '__return_true' significa pubblico, ma va usato consapevolmente:
add_action( 'rest_api_init', function () {
register_rest_route( 'wp-planet/v1', '/stats', array(
'methods' => 'GET',
'callback' => 'wpplanet_get_site_stats',
'permission_callback' => '__return_true', // endpoint pubblico, nessun dato sensibile
'args' => array(
'range' => array(
'type' => 'string',
'default' => '30d',
'enum' => array( '7d', '30d', '90d' ),
),
),
) );
} );
function wpplanet_get_site_stats( WP_REST_Request $request ) {
$range = $request->get_param( 'range' );
// Recupera solo i campi necessari — niente oggetto post completo
$stats = array(
'posts' => wp_count_posts( 'post' )->publish,
'pages' => wp_count_posts( 'page' )->publish,
'comments' => wp_count_comments()->approved,
'updated' => current_time( 'c' ),
);
// Cache lato server: 5 minuti per non rifare le query ogni richiesta
return new WP_REST_Response( $stats, 200, array(
'Cache-Control' => 'max-age=300, s-maxage=300',
) );
}Validare l'input (perché WordPress non lo fa per te)
Gli args della registrazione definiscono i tipi, ma WordPress non li valida in modo rigoroso di default. Un 'type' => 'integer' accetta ancora una stringa se il cast implicito funziona. Per endpoint che accettano input complesso, valida esplicitamente nel callback — o con un plugin di validazione, o con controlli manuali. Un endpoint che accetta un ID senza verificare che sia un intero positivo è un invito a injection e a query pesanti.
Per endpoint che accettano POST con payload JSON, usa get_json_params() invece dei parametri form. E valida ogni campo:
function wpplanet_create_ticket( WP_REST_Request $request ) {
$body = $request->get_json_params();
// Validazione manuale — ogni campo obbligatorio
$required = array( 'subject', 'description', 'site_url' );
foreach ( $required as $field ) {
if ( empty( $body[ $field ] ) ) {
return new WP_Error(
'missing_field',
sprintf( 'Campo obbligatorio mancante: %s', $field ),
array( 'status' => 400 )
);
}
}
// Sanitizza ogni input
$subject = sanitize_text_field( $body['subject'] );
$description = sanitize_textarea_field( $body['description'] );
$site_url = esc_url_raw( $body['site_url'] );
// Verifica che site_url sia un URL valido
if ( ! filter_var( $site_url, FILTER_VALIDATE_URL ) ) {
return new WP_Error( 'invalid_url', 'URL del sito non valido', array( 'status' => 400 ) );
}
// ... logica di creazione del ticket ...
return new WP_REST_Response( array( 'id' => $ticket_id ), 201 );
}Autenticazione: application passwords non basta per tutto
L'autenticazione di default della REST API usa i cookie di sessione di WordPress — funziona solo se la richiesta viene dallo stesso dominio (con il nonce wp_rest). Per richieste esterne (app, CRM, servizi terzi), le application passwords sono il metodo più semplice: generi una password applicazione dal profilo utente e la invii come Basic Auth. Funziona, ma ha limiti: ogni password è legata a un utente, non puoi scaderla facilmente, e se l'app la perde hai un problema.
Per endpoint che richiedono sicurezza maggiore — operazioni amministrative, accesso a dati sensibili — usa un token JWT o un custom API key system. Un pattern che vediamo spesso è un endpoint di autenticazione che restituisce un token a scadenza, e gli altri endpoint verificano il token:
// Verifica un custom API key header su endpoint protetti
add_filter( 'rest_authentication_errors', function( $result ) {
// Se c'è già un errore (es. nonce mancante), lascialo passare
if ( ! empty( $result ) ) {
return $result;
}
// Solo per le route custom wp-planet/v1
$route = $_SERVER['REQUEST_URI'] ?? '';
if ( strpos( $route, 'wp-planet/v1' ) === false ) {
return $result;
}
// Endpoint pubblici non richiedono autenticazione
$public_routes = array( '/stats', '/public-posts' );
foreach ( $public_routes as $public ) {
if ( strpos( $route, $public ) !== false ) {
return $result; // pubblico, lascia passare
}
}
// Verifica l'API key header
$api_key = $_SERVER['HTTP_X_WPPLANET_KEY'] ?? '';
$stored_key = get_option( 'wpplanet_api_key' );
if ( ! $stored_key || ! hash_equals( $stored_key, $api_key ) ) {
return new WP_Error(
'rest_forbidden',
'API key non valida o mancante',
array( 'status' => 401 )
);
}
return $result;
} );Il test che ti dice se l'endpoint è performante
Il modo più rapido di misurare un endpoint è curl con -w per il timing. Un endpoint REST su WordPress dovrebbe rispondere sotto i 200ms su una cache calda. Se ci mette 800ms o più, il problema è quasi sempre una query non ottimizzata o un get_posts che carica troppo:
# Misura tempo di risposta e dimensione del payload
curl -o /dev/null -s -w 'Tempo: %{time_total}s\nDimensione: %{size_download} byte\n' \
-H 'X-WPPlanet-Key: tua-api-key' \
'https://www.example.com/wp-json/wp-planet/v1/stats?range=30d'
# Test di carico rapido: 20 richieste consecutive
for i in $(seq 1 20); do
curl -o /dev/null -s -w '%{time_total}\n' \
'https://www.example.com/wp-json/wp-planet/v1/stats?range=30d'
doneUn endpoint REST ben scritto è invisibile: risponde veloce, restituisce solo lo stretto necessario, valida tutto, e non si fa notare. Uno scritto male rallenta il sito, apre falle di sicurezza, e diventa un collo di bottiglia che si scopre solo quando il traffico cresce. La differenza, quasi sempre, è nei dettagli che nessuno controlla.
Codice pulito, prestazioni e dipendenze ridotte al minimo.
Sviluppo WordPress personalizzato: temi leggeri, plugin su misura e integrazioni API senza appesantire il sito.
Articoli correlati

Testing e CI/CD per WordPress: pipeline automatizzate per non rompere il sito in produzione
Leggi
Accessibilità WCAG 2.2 e WordPress: conformità AA senza plugin, con codice
Leggi
