Özel Endpoint ile WordPress REST API Genişletme
REST API’yi ilk gördüğümde “bu ne işime yarayacak ki” diye düşünmüştüm. Yıllar sonra şunu söyleyebilirim: WordPress projelerinin yarısında özel endpoint yazmadan işin içinden çıkmak mümkün değil. Mobil uygulama entegrasyonu, harici servislerle veri alışverişi, SPA frontend’ler… Hepsi sizi er ya da geç özel bir REST endpoint’i yazmaya itiyor.
Bu yazıda sizi teoride boğmadan, gerçekten sahada kullandığım yöntemlerle WordPress REST API’sini nasıl genişleteceğinizi anlatacağım.
REST API’nin Temel Mantığını Kavramak
WordPress REST API, /wp-json/ yolu altında çalışır. Yerleşik namespace’ler var: wp/v2 posts, users, categories gibi core içerikleri sunar. Siz kendi eklentiniz için myplugin/v1 gibi bir namespace tanımlarsınız ve bu alan tamamen size ait olur.
Endpoint kaydetmek için register_rest_route() fonksiyonu kullanılır ve bu fonksiyonu rest_api_init hook’una bağlamak zorundasınız. Aksi halde API hazır olmadan kayıt yapmaya çalışırsınız, beklenmedik davranışlarla karşılaşırsınız.
add_action( 'rest_api_init', function() {
register_rest_route( 'myplugin/v1', '/products', array(
'methods' => 'GET',
'callback' => 'myplugin_get_products',
'permission_callback' => '__return_true',
) );
} );
permission_callback parametresi kritik. __return_true geçerseniz endpoint herkese açık olur. Kimlik doğrulama gerektiren bir yapı için bu değeri bir fonksiyonla değiştirmeniz gerekiyor, aşağıda bunu ele alacağız.
Gerçek Bir Senaryo: Stok Durumu Sorgulama Endpoint’i
Diyelim ki bir WooCommerce mağazanız var ve harici bir depo yönetim sistemi ürün stok durumlarını sorgulamak istiyor. Core WooCommerce API’si bu iş için kullanılabilir ama yetkisiz erişim kapıları açmak istemiyorsunuz, üstelik sadece stok bilgisi lazım, tüm ürün verisini döndürmek gereksiz.
add_action( 'rest_api_init', function() {
register_rest_route( 'depo/v1', '/stok/(?P<sku>[a-zA-Z0-9_-]+)', array(
'methods' => WP_REST_Server::READABLE,
'callback' => 'depo_stok_sorgula',
'permission_callback' => 'depo_api_yetki_kontrol',
'args' => array(
'sku' => array(
'required' => true,
'validate_callback' => function( $param ) {
return is_string( $param ) && strlen( $param ) > 0;
},
),
),
) );
} );
Callback fonksiyonunu yazalım:
function depo_stok_sorgula( WP_REST_Request $request ) {
$sku = sanitize_text_field( $request->get_param( 'sku' ) );
$product_id = wc_get_product_id_by_sku( $sku );
if ( ! $product_id ) {
return new WP_Error(
'urun_bulunamadi',
'Bu SKU'ya ait ürün bulunamadı.',
array( 'status' => 404 )
);
}
$product = wc_get_product( $product_id );
if ( ! $product ) {
return new WP_Error(
'urun_yuklenemedi',
'Ürün yüklenirken bir hata oluştu.',
array( 'status' => 500 )
);
}
$data = array(
'sku' => $sku,
'stok_durumu' => $product->get_stock_status(),
'stok_miktari' => $product->get_stock_quantity(),
'yonetiliyor' => $product->managing_stock(),
);
return rest_ensure_response( $data );
}
Yetki kontrolü için API anahtarı tabanlı basit bir yapı:
function depo_api_yetki_kontrol( WP_REST_Request $request ) {
$api_key = $request->get_header( 'X-Depo-Api-Key' );
if ( empty( $api_key ) ) {
return new WP_Error(
'yetkisiz',
'API anahtarı gereklidir.',
array( 'status' => 401 )
);
}
$gecerli_anahtar = get_option( 'depo_api_key' );
if ( ! hash_equals( $gecerli_anahtar, $api_key ) ) {
return new WP_Error(
'gecersiz_anahtar',
'API anahtarı geçersiz.',
array( 'status' => 403 )
);
}
return true;
}
hash_equals() kullanımına dikkat edin. Timing attack’lara karşı düz string karşılaştırması yerine her zaman bu fonksiyonu tercih edin.
POST Endpoint’i ve Veri Doğrulama
Harici bir sistemin WordPress’e veri göndermesi gerektiğinde POST endpoint’leri devreye giriyor. Burada veri doğrulama hayati önem taşıyor. REST API’nin args sistemi bu iş için biçilmiş kaftan.
add_action( 'rest_api_init', function() {
register_rest_route( 'depo/v1', '/siparis-guncelle', array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => 'depo_siparis_guncelle',
'permission_callback' => 'depo_api_yetki_kontrol',
'args' => array(
'siparis_id' => array(
'required' => true,
'type' => 'integer',
'minimum' => 1,
'sanitize_callback' => 'absint',
'validate_callback' => function( $param ) {
return is_numeric( $param ) && $param > 0;
},
),
'durum' => array(
'required' => true,
'type' => 'string',
'enum' => array( 'hazirlanıyor', 'kargoda', 'teslim_edildi', 'iptal' ),
'sanitize_callback' => 'sanitize_text_field',
),
'kargo_kodu' => array(
'required' => false,
'type' => 'string',
'default' => '',
'sanitize_callback' => 'sanitize_text_field',
),
),
) );
} );
function depo_siparis_guncelle( WP_REST_Request $request ) {
$siparis_id = $request->get_param( 'siparis_id' );
$durum = $request->get_param( 'durum' );
$kargo_kodu = $request->get_param( 'kargo_kodu' );
$siparis = wc_get_order( $siparis_id );
if ( ! $siparis ) {
return new WP_Error(
'siparis_bulunamadi',
"#{$siparis_id} numaralı sipariş bulunamadı.",
array( 'status' => 404 )
);
}
$durum_haritasi = array(
'hazirlanıyor' => 'processing',
'kargoda' => 'on-hold',
'teslim_edildi' => 'completed',
'iptal' => 'cancelled',
);
$siparis->update_status( $durum_haritasi[ $durum ], "Depo sistemi güncellemesi: {$durum}" );
if ( ! empty( $kargo_kodu ) ) {
$siparis->update_meta_data( '_kargo_takip_kodu', sanitize_text_field( $kargo_kodu ) );
$siparis->save();
}
return rest_ensure_response( array(
'basarili' => true,
'siparis_id' => $siparis_id,
'yeni_durum' => $siparis->get_status(),
'guncellendi' => current_time( 'mysql' ),
) );
}
enum validasyonu sayesinde izin verilmeyen durum değerleri otomatik olarak reddediliyor, ayrıca manuel kontrol yazmanıza gerek kalmıyor.
WP_REST_Controller Sınıfını Kullanmak
Birden fazla endpoint’i olan bir eklenti geliştiriyorsanız, fonksiyon tabanlı yaklaşım yerine WP_REST_Controller sınıfını kalıtmak çok daha sürdürülebilir bir kod tabanı sağlıyor. Bu yaklaşım başlangıçta biraz fazla karmaşık görünebilir ama proje büyüdüğünde neden böyle yapıldığını anlıyorsunuz.
class Depo_REST_Controller extends WP_REST_Controller {
public function __construct() {
$this->namespace = 'depo/v1';
$this->rest_base = 'urunler';
}
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<id>[d]+)',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
'id' => array(
'description' => 'Ürün ID'si.',
'type' => 'integer',
),
),
),
)
);
}
public function get_items_permissions_check( $request ) {
return depo_api_yetki_kontrol( $request );
}
public function get_item_permissions_check( $request ) {
return depo_api_yetki_kontrol( $request );
}
public function get_items( $request ) {
$args = array(
'post_type' => 'product',
'posts_per_page' => $request->get_param( 'per_page' ) ?? 10,
'paged' => $request->get_param( 'page' ) ?? 1,
);
$query = new WP_Query( $args );
$urunler = array();
foreach ( $query->posts as $post ) {
$urun = wc_get_product( $post->ID );
$urunler[] = $this->prepare_item_for_response( $urun, $request );
}
$yanit = rest_ensure_response( $urunler );
$yanit->header( 'X-WP-Total', $query->found_posts );
$yanit->header( 'X-WP-TotalPages', $query->max_num_pages );
return $yanit;
}
public function get_item( $request ) {
$id = (int) $request->get_param( 'id' );
$urun = wc_get_product( $id );
if ( ! $urun ) {
return new WP_Error( 'bulunamadi', 'Ürün bulunamadı.', array( 'status' => 404 ) );
}
return rest_ensure_response( $this->prepare_item_for_response( $urun, $request ) );
}
public function prepare_item_for_response( $item, $request ) {
return array(
'id' => $item->get_id(),
'adi' => $item->get_name(),
'sku' => $item->get_sku(),
'fiyat' => $item->get_price(),
'stok_durumu' => $item->get_stock_status(),
'stok_miktari' => $item->get_stock_quantity(),
);
}
}
add_action( 'rest_api_init', function() {
$controller = new Depo_REST_Controller();
$controller->register_routes();
} );
Mevcut Core Endpoint’lerini Genişletmek
Bazen sıfırdan endpoint yazmak yerine mevcut endpoint’lere yeni alan eklemek daha pratik oluyor. register_rest_field() tam bu iş için var.
Örneğin wp/v2/posts yanıtına özel bir meta alanı eklemek istiyorsunuz:
add_action( 'rest_api_init', function() {
register_rest_field(
'post',
'okuma_suresi',
array(
'get_callback' => function( $post_arr ) {
$icerik = get_post_field( 'post_content', $post_arr['id'] );
$kelime = str_word_count( wp_strip_all_tags( $icerik ) );
$dakika = ceil( $kelime / 200 );
return $dakika . ' dakika';
},
'update_callback' => null,
'schema' => array(
'description' => 'Tahmini okuma süresi.',
'type' => 'string',
'context' => array( 'view' ),
),
)
);
} );
Artık /wp-json/wp/v2/posts isteğine gelen yanıtta her post için okuma_suresi alanı otomatik olarak gelecek.
Hata Yönetimi ve Loglama
Sahada öğrendiğim en önemli şeylerden biri: REST endpoint’lerinizde düzgün hata yönetimi olmadan debug süreci cehenneme dönüyor. Özellikle harici sistemler entegrasyonunda “neden 500 döndü” sorusunun cevabını bulmak saatler alabilir.
function depo_guvenli_islem( callable $islem, WP_REST_Request $request ) {
try {
return $islem( $request );
} catch ( InvalidArgumentException $e ) {
depo_hata_logla( 'gecersiz_arguman', $e->getMessage(), $request );
return new WP_Error(
'gecersiz_istek',
'Geçersiz parametreler gönderildi.',
array( 'status' => 400 )
);
} catch ( Exception $e ) {
depo_hata_logla( 'genel_hata', $e->getMessage(), $request );
return new WP_Error(
'sunucu_hatasi',
'İşlem sırasında bir hata oluştu.',
array( 'status' => 500 )
);
}
}
function depo_hata_logla( string $tip, string $mesaj, WP_REST_Request $request ) {
if ( ! defined( 'WP_DEBUG_LOG' ) || ! WP_DEBUG_LOG ) {
return;
}
$log = sprintf(
'[DEPO API HATA] Tip: %s | Mesaj: %s | Endpoint: %s | IP: %s | Zaman: %s',
$tip,
$mesaj,
$request->get_route(),
$_SERVER['REMOTE_ADDR'] ?? 'bilinmiyor',
current_time( 'mysql' )
);
error_log( $log );
}
Güvenlik: Rate Limiting
Özel endpoint’leri dışarıya açıyorsanız mutlaka bir rate limiting mekanizması kurmalısınız. WordPress’in bunu yapacak yerleşik bir aracı yok ama transient tabanlı basit bir çözüm çoğu senaryo için yeterli:
function depo_rate_limit_kontrol( string $ip, int $limit = 60, int $pencere = 60 ): bool {
$anahtar = 'depo_rl_' . md5( $ip );
$mevcut = (int) get_transient( $anahtar );
if ( $mevcut >= $limit ) {
return false;
}
if ( $mevcut === 0 ) {
set_transient( $anahtar, 1, $pencere );
} else {
set_transient( $anahtar, $mevcut + 1, $pencere );
}
return true;
}
function depo_api_yetki_kontrol_gelismis( WP_REST_Request $request ) {
$ip = $_SERVER['REMOTE_ADDR'] ?? '';
if ( ! depo_rate_limit_kontrol( $ip, 100, 60 ) ) {
return new WP_Error(
'cok_fazla_istek',
'Çok fazla istek gönderildi. Lütfen bekleyiniz.',
array( 'status' => 429 )
);
}
return depo_api_yetki_kontrol( $request );
}
Yoğun trafikli ortamlarda transient tabanlı rate limiting veritabanını zorlayabilir. O durumda Redis ile object cache kullanıyorsanız bu yaklaşım çok daha performanslı çalışır.
Endpoint’i Test Etmek
Geliştirme sırasında endpoint’leri test etmek için birkaç pratik yöntem var.
cURL ile hızlı test:
# GET isteği
curl -X GET
"https://siteniz.com/wp-json/depo/v1/stok/ABC-123"
-H "X-Depo-Api-Key: sizin-api-anahtariniz"
-H "Accept: application/json"
# POST isteği
curl -X POST
"https://siteniz.com/wp-json/depo/v1/siparis-guncelle"
-H "X-Depo-Api-Key: sizin-api-anahtariniz"
-H "Content-Type: application/json"
-d '{"siparis_id": 1234, "durum": "kargoda", "kargo_kodu": "TK123456789TR"}'
WordPress CLI ile endpoint listesini görmek de çok kullanışlı:
wp rest-api list-routes --namespace=depo/v1 --format=table
Namespace Versiyonlama Stratejisi
Bir API yayına girdikten sonra breaking change yapmak, onu kullanan sistemleri kırıyor. Bu yüzden /v1, /v2 şeklinde versiyonlama şart. Yeni bir versiyon çıkardığınızda eski versiyonu bir süre daha ayakta tutun, deprecated header ekleyin:
add_filter( 'rest_post_dispatch', function( $yanit, $sunucu, $istek ) {
if ( strpos( $istek->get_route(), '/depo/v1/' ) === 0 ) {
$yanit->header( 'X-Api-Deprecated', 'true' );
$yanit->header( 'X-Api-Sunset-Date', '2025-12-31' );
$yanit->header( 'X-Api-Successor', '/wp-json/depo/v2/' );
}
return $yanit;
}, 10, 3 );
Bu sayede entegrasyon yapan ekipler hangi endpoint’in kullanım ömrünü tamamlamak üzere olduğunu anlık olarak takip edebilir.
Sonuç
WordPress REST API’sini genişletmek ilk bakışta karmaşık görünse de temel prensipler oldukça tutarlı: rest_api_init hook’u, register_rest_route(), sağlam bir permission callback ve düzgün veri doğrulama. Bu dört taşı yerine oturttuğunuzda gerisi detay.
Özellikle dikkat etmeniz gereken noktalar:
- Yetki kontrolünü asla atlamayın:
__return_truesadece gerçekten herkese açık olması gereken endpoint’ler için kullanın - Sanitize ve validate ayrımını bilin:
sanitize_callbackveriyi temizler,validate_callbackformatı kontrol eder, ikisi farklı işler yapar rest_ensure_response()kullanın: Düz array döndürmek yerine her zaman bu wrapper’ı kullanın- Versiyonlamaya baştan karar verin:
v1namespace ile başlamak sonradan kurtarıcı olur - Hataları
WP_Errorile döndürün: Doğrudan HTTP status kodu yazmak yerine WordPress’in hata mekanizmasını kullanın
Harici sistemlerle entegrasyon projelerinde özel endpoint mimarisi, hem güvenlik hem sürdürülebilirlik açısından sizi çok rahat ettiriyor. Bir kez düzgün kurguladığınızda aylarca bakım gerektirmeyen, sağlam bir API katmanı elde ediyorsunuz.
