WordPress REST API’ye Özel Endpoint Ekleme

WordPress’in REST API’si, modern web geliştirmenin vazgeçilmez bir parçası haline geldi. Headless WordPress projeleri, mobil uygulamalar, üçüncü parti entegrasyonlar… Bunların hepsinin temelinde REST API yatıyor. Ama bazen WordPress’in bize sunduğu hazır endpoint’ler yetmiyor. Kendi iş mantığınızı, özel sorgularınızı veya harici entegrasyonlarınızı REST API üzerinden sunmak istiyorsunuz. İşte tam bu noktada functions.php dosyanıza birkaç satır kod ekleyerek işi çözebilirsiniz.

Bu yazıda, WordPress REST API’ye nasıl özel endpoint ekleyeceğinizi, kimlik doğrulamayı nasıl yöneteceğinizi, parametreleri nasıl validate edeceğinizi ve gerçek dünya senaryolarında bu bilgiyi nasıl kullanacağınızı adım adım anlatacağım.

WordPress REST API’nin Temel Mantığı

WordPress REST API, /wp-json/ yolu altında çalışır. Yerleşik endpoint’ler wp/v2 namespace’ini kullanır. Siz kendi endpoint’lerinizi tanımlarken kendi namespace’inizi belirlemelisiniz. Bu, olası çakışmaları önler ve API’nizin versiyonlanmasını kolaylaştırır.

Özel endpoint eklemek için temel araç register_rest_route() fonksiyonudur. Bu fonksiyonu rest_api_init action hook’u içinde çağırmanız gerekir.

add_action( 'rest_api_init', function () {
    register_rest_route( 'myplugin/v1', '/ornek', array(
        'methods'             => 'GET',
        'callback'            => 'myplugin_ornek_callback',
        'permission_callback' => '__return_true',
    ) );
} );

function myplugin_ornek_callback( WP_REST_Request $request ) {
    return new WP_REST_Response( array( 'mesaj' => 'Merhaba REST API!' ), 200 );
}

Bu kadar basit. Ama tabii ki gerçek hayat bu kadar basit değil. Şimdi katmanları tek tek açalım.

register_rest_route Parametreleri

register_rest_route() fonksiyonu üç ana parametre alır:

  • namespace: API’nizin namespace’i. Genellikle eklenti-adı/v1 formatında olur.
  • route: Endpoint yolu. Regex destekler, URL parametreleri alabilir.
  • args: Endpoint davranışını tanımlayan dizi.

args dizisi içinde kullanabileceğiniz seçenekler:

  • methods: HTTP metodu. GET, POST, PUT, PATCH, DELETE veya WP_REST_Server::READABLE, WP_REST_Server::CREATABLE gibi sabitler kullanabilirsiniz.
  • callback: İstek geldiğinde çalışacak fonksiyon. WP_REST_Request nesnesi alır.
  • permission_callback: Kimlik doğrulama fonksiyonu. true dönerse istek işlenir, WP_Error dönerse reddedilir.
  • args: İstek parametrelerini tanımlayan dizi. Validation ve sanitization burada yapılır.

İlk Gerçek Senaryo: Özel Ürün Listesi Endpoint’i

Bir WooCommerce sitesi yönettiğinizi düşünün. Mobil uygulama ekibi sizden belirli kategorideki ürünleri, özel meta alanlarıyla birlikte döndüren bir endpoint istiyor. Standart WooCommerce REST API endpoint’i ihtiyacı karşılamıyor çünkü hesaplanan bazı alanlar ve özel sıralama mantığı var.

add_action( 'rest_api_init', function () {
    register_rest_route( 'mystore/v1', '/ozel-urunler', array(
        'methods'             => WP_REST_Server::READABLE,
        'callback'            => 'mystore_ozel_urunler_getir',
        'permission_callback' => '__return_true',
        'args'                => array(
            'kategori_id' => array(
                'required'          => true,
                'validate_callback' => function( $param ) {
                    return is_numeric( $param ) && $param > 0;
                },
                'sanitize_callback' => 'absint',
                'description'       => 'WooCommerce kategori ID',
            ),
            'limit' => array(
                'required'          => false,
                'default'           => 10,
                'validate_callback' => function( $param ) {
                    return is_numeric( $param ) && $param <= 50;
                },
                'sanitize_callback' => 'absint',
            ),
        ),
    ) );
} );

function mystore_ozel_urunler_getir( WP_REST_Request $request ) {
    $kategori_id = $request->get_param( 'kategori_id' );
    $limit       = $request->get_param( 'limit' );

    $args = array(
        'post_type'      => 'product',
        'posts_per_page' => $limit,
        'post_status'    => 'publish',
        'tax_query'      => array(
            array(
                'taxonomy' => 'product_cat',
                'field'    => 'term_id',
                'terms'    => $kategori_id,
            ),
        ),
    );

    $urunler  = new WP_Query( $args );
    $sonuclar = array();

    if ( $urunler->have_posts() ) {
        while ( $urunler->have_posts() ) {
            $urunler->the_post();
            $urun_id   = get_the_ID();
            $wc_urun   = wc_get_product( $urun_id );

            $sonuclar[] = array(
                'id'           => $urun_id,
                'isim'         => get_the_title(),
                'fiyat'        => $wc_urun->get_price(),
                'stok_durumu'  => $wc_urun->get_stock_status(),
                'ozel_alan'    => get_post_meta( $urun_id, '_ozel_alan', true ),
                'url'          => get_permalink(),
                'gorsel'       => get_the_post_thumbnail_url( $urun_id, 'medium' ),
            );
        }
        wp_reset_postdata();
    }

    if ( empty( $sonuclar ) ) {
        return new WP_Error(
            'urun_bulunamadi',
            'Bu kategoride ürün bulunamadı.',
            array( 'status' => 404 )
        );
    }

    return new WP_REST_Response( $sonuclar, 200 );
}

Bu endpoint’i test etmek için tarayıcınızda şu URL’yi açabilirsiniz:

curl -X GET "https://siteniz.com/wp-json/mystore/v1/ozel-urunler?kategori_id=5&limit=3"

Kimlik Doğrulama: permission_callback Kullanımı

Herkese açık endpoint’ler için __return_true yeterli. Ama hassas verilere erişim veya veri yazma işlemleri için mutlaka kimlik doğrulama yapmalısınız. __return_false kullanırsanız tüm istekler 401 döner, bu da yanlış bir yaklaşım.

Kullanıcının giriş yapmış olup olmadığını kontrol eden basit bir örnek:

add_action( 'rest_api_init', function () {
    register_rest_route( 'mysite/v1', '/kullanici-bilgisi', array(
        'methods'             => WP_REST_Server::READABLE,
        'callback'            => 'mysite_kullanici_bilgisi',
        'permission_callback' => 'mysite_giris_kontrol',
    ) );

    register_rest_route( 'mysite/v1', '/admin-raporu', array(
        'methods'             => WP_REST_Server::READABLE,
        'callback'            => 'mysite_admin_raporu',
        'permission_callback' => 'mysite_admin_kontrol',
    ) );
} );

function mysite_giris_kontrol() {
    return is_user_logged_in();
}

function mysite_admin_kontrol() {
    return current_user_can( 'manage_options' );
}

function mysite_kullanici_bilgisi( WP_REST_Request $request ) {
    $kullanici = wp_get_current_user();
    return new WP_REST_Response( array(
        'id'    => $kullanici->ID,
        'isim'  => $kullanici->display_name,
        'email' => $kullanici->user_email,
        'roller' => $kullanici->roles,
    ), 200 );
}

function mysite_admin_raporu( WP_REST_Request $request ) {
    global $wpdb;
    $toplam_uye = $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->users}" );
    $toplam_yazi = wp_count_posts( 'post' )->publish;

    return new WP_REST_Response( array(
        'toplam_uye'  => (int) $toplam_uye,
        'toplam_yazi' => (int) $toplam_yazi,
        'tarih'       => current_time( 'mysql' ),
    ), 200 );
}

Application Passwords ile Bearer Token Doğrulama

WordPress 5.6 ile gelen Application Passwords özelliği, REST API kimlik doğrulaması için çok pratik bir çözüm sunuyor. Kullanıcı profil sayfasından uygulama şifresi oluşturabilirsiniz. İstek gönderirken:

curl -X GET "https://siteniz.com/wp-json/mysite/v1/kullanici-bilgisi" 
  -H "Authorization: Basic $(echo -n 'kullanici_adi:uygulama_sifresi' | base64)"

permission_callback içinde current_user_can() veya is_user_logged_in() kontrolleri bu kimlik doğrulama yöntemiyle otomatik çalışır. WordPress, Authorization header’ını kendisi parse eder.

URL Parametreli Dinamik Endpoint’ler

Regex kullanarak URL’den dinamik parametre yakalayabilirsiniz. Mesela /kullanici/123/siparisler gibi bir yapı:

add_action( 'rest_api_init', function () {
    register_rest_route( 'mystore/v1', '/kullanici/(?P<kullanici_id>d+)/siparisler', array(
        'methods'             => WP_REST_Server::READABLE,
        'callback'            => 'mystore_kullanici_siparisler',
        'permission_callback' => 'mystore_siparis_yetki_kontrol',
        'args'                => array(
            'kullanici_id' => array(
                'validate_callback' => function( $param ) {
                    return is_numeric( $param );
                },
            ),
        ),
    ) );
} );

function mystore_siparis_yetki_kontrol( WP_REST_Request $request ) {
    $kullanici_id = $request->get_param( 'kullanici_id' );
    $mevcut_kullanici = get_current_user_id();

    // Admin her kullanıcının siparişini görebilir
    if ( current_user_can( 'manage_woocommerce' ) ) {
        return true;
    }

    // Kullanıcı sadece kendi siparişlerini görebilir
    return (int) $kullanici_id === (int) $mevcut_kullanici;
}

function mystore_kullanici_siparisler( WP_REST_Request $request ) {
    $kullanici_id = absint( $request->get_param( 'kullanici_id' ) );

    $siparisler = wc_get_orders( array(
        'customer_id' => $kullanici_id,
        'limit'       => 20,
        'orderby'     => 'date',
        'order'       => 'DESC',
    ) );

    $veri = array();
    foreach ( $siparisler as $siparis ) {
        $veri[] = array(
            'siparis_id'  => $siparis->get_id(),
            'durum'       => $siparis->get_status(),
            'toplam'      => $siparis->get_total(),
            'tarih'       => $siparis->get_date_created()->date( 'Y-m-d H:i:s' ),
            'urun_sayisi' => count( $siparis->get_items() ),
        );
    }

    return new WP_REST_Response( $veri, 200 );
}

POST İsteği ile Veri Yazma

Okuma işlemlerinin yanında veri yazma da sık ihtiyaç duyulan bir senaryo. Bir iletişim formu veya potansiyel müşteri kaydı endpoint’i:

add_action( 'rest_api_init', function () {
    register_rest_route( 'mysite/v1', '/potansiyel-musteri', array(
        'methods'             => WP_REST_Server::CREATABLE,
        'callback'            => 'mysite_potansiyel_musteri_kaydet',
        'permission_callback' => '__return_true',
        'args'                => array(
            'ad' => array(
                'required'          => true,
                'type'              => 'string',
                'sanitize_callback' => 'sanitize_text_field',
                'validate_callback' => function( $param ) {
                    return ! empty( trim( $param ) ) && strlen( $param ) <= 100;
                },
            ),
            'email' => array(
                'required'          => true,
                'type'              => 'string',
                'sanitize_callback' => 'sanitize_email',
                'validate_callback' => function( $param ) {
                    return is_email( $param );
                },
            ),
            'telefon' => array(
                'required'          => false,
                'type'              => 'string',
                'sanitize_callback' => 'sanitize_text_field',
            ),
            'mesaj' => array(
                'required'          => false,
                'type'              => 'string',
                'sanitize_callback' => 'sanitize_textarea_field',
            ),
        ),
    ) );
} );

function mysite_potansiyel_musteri_kaydet( WP_REST_Request $request ) {
    // Nonce kontrolü eklenebilir, rate limiting için
    $ad      = $request->get_param( 'ad' );
    $email   = $request->get_param( 'email' );
    $telefon = $request->get_param( 'telefon' );
    $mesaj   = $request->get_param( 'mesaj' );

    // Duplicate kontrolü
    global $wpdb;
    $mevcut = $wpdb->get_var( $wpdb->prepare(
        "SELECT COUNT(*) FROM {$wpdb->prefix}potansiyel_musteriler WHERE email = %s",
        $email
    ) );

    if ( $mevcut > 0 ) {
        return new WP_Error(
            'duplicate_email',
            'Bu e-posta adresi zaten kayıtlı.',
            array( 'status' => 409 )
        );
    }

    // Kayıt işlemi
    $sonuc = $wpdb->insert(
        $wpdb->prefix . 'potansiyel_musteriler',
        array(
            'ad'           => $ad,
            'email'        => $email,
            'telefon'      => $telefon,
            'mesaj'        => $mesaj,
            'kayit_tarihi' => current_time( 'mysql' ),
        ),
        array( '%s', '%s', '%s', '%s', '%s' )
    );

    if ( false === $sonuc ) {
        return new WP_Error(
            'kayit_hatasi',
            'Kayıt sırasında bir hata oluştu.',
            array( 'status' => 500 )
        );
    }

    // Admin'e bildirim emaili
    wp_mail(
        get_option( 'admin_email' ),
        'Yeni Potansiyel Müşteri: ' . $ad,
        "Ad: {$ad}nEmail: {$email}nTelefon: {$telefon}nMesaj: {$mesaj}"
    );

    return new WP_REST_Response(
        array(
            'basarili' => true,
            'mesaj'    => 'Bilgileriniz alındı, en kısa sürede iletişime geçeceğiz.',
            'id'       => $wpdb->insert_id,
        ),
        201
    );
}

Bu endpoint’i test etmek:

curl -X POST "https://siteniz.com/wp-json/mysite/v1/potansiyel-musteri" 
  -H "Content-Type: application/json" 
  -d '{"ad": "Ahmet Yılmaz", "email": "[email protected]", "mesaj": "Bilgi almak istiyorum"}'

Birden Fazla HTTP Metodu Destekleme

Bazen aynı endpoint’te hem GET hem POST desteklemek isteyebilirsiniz. register_rest_route‘u birden fazla kez çağırarak veya methods parametresine dizi geçerek bunu yapabilirsiniz:

add_action( 'rest_api_init', function () {
    // GET - Liste getir
    register_rest_route( 'mysite/v1', '/notlar', array(
        array(
            'methods'             => WP_REST_Server::READABLE,
            'callback'            => 'mysite_notlari_getir',
            'permission_callback' => 'mysite_giris_kontrol',
        ),
        array(
            'methods'             => WP_REST_Server::CREATABLE,
            'callback'            => 'mysite_not_ekle',
            'permission_callback' => 'mysite_giris_kontrol',
            'args'                => array(
                'baslik' => array(
                    'required'          => true,
                    'sanitize_callback' => 'sanitize_text_field',
                ),
                'icerik' => array(
                    'required'          => true,
                    'sanitize_callback' => 'wp_kses_post',
                ),
            ),
        ),
    ) );

    // Tekil not için GET, PUT, DELETE
    register_rest_route( 'mysite/v1', '/notlar/(?P<id>d+)', array(
        array(
            'methods'             => WP_REST_Server::READABLE,
            'callback'            => 'mysite_not_getir',
            'permission_callback' => 'mysite_giris_kontrol',
        ),
        array(
            'methods'             => WP_REST_Server::DELETABLE,
            'callback'            => 'mysite_not_sil',
            'permission_callback' => 'mysite_giris_kontrol',
        ),
    ) );
} );

Rate Limiting ve Güvenlik Önlemleri

Açık endpoint’lerde özellikle, aşırı istek geldiğinde sitenizi korumak önemli. WordPress’in built-in rate limiting’i yok, ama basit bir transient tabanlı çözümle başlangıç yapabilirsiniz:

function mysite_rate_limit_kontrol( WP_REST_Request $request ) {
    $ip           = $_SERVER['REMOTE_ADDR'];
    $transient_key = 'rl_' . md5( $ip );
    $istek_sayisi  = get_transient( $transient_key );

    if ( false === $istek_sayisi ) {
        set_transient( $transient_key, 1, MINUTE_IN_SECONDS );
    } elseif ( $istek_sayisi < 30 ) {
        set_transient( $transient_key, $istek_sayisi + 1, MINUTE_IN_SECONDS );
    } else {
        return new WP_Error(
            'rate_limit_asildi',
            'Çok fazla istek gönderildi. Lütfen bir dakika sonra tekrar deneyin.',
            array( 'status' => 429 )
        );
    }

    return true;
}

Bu fonksiyonu permission_callback olarak kullanabilir veya mevcut permission callback’inizin içinde çağırabilirsiniz. Dikkat edin, permission_callback‘den WP_Error döndürmek işe yarar ama false döndürmek 401 verir. Rate limit için 429 dönmek istiyorsanız WP_Error kullanmalısınız.

Schema ve Belgelendirme

REST API’nizi kullanan geliştiriciler için schema tanımlamak, API’nizin otomatik belgelenmesini sağlar. OPTIONS isteğiyle endpoint şeması sorgulanabilir:

add_action( 'rest_api_init', function () {
    register_rest_route( 'mysite/v1', '/makaleler', array(
        'methods'             => WP_REST_Server::READABLE,
        'callback'            => 'mysite_makaleler_getir',
        'permission_callback' => '__return_true',
        'schema'              => 'mysite_makale_schema',
    ) );
} );

function mysite_makale_schema() {
    return array(
        '$schema'    => 'http://json-schema.org/draft-04/schema#',
        'title'      => 'makale',
        'type'       => 'object',
        'properties' => array(
            'id' => array(
                'description' => 'Makale benzersiz tanımlayıcı.',
                'type'        => 'integer',
                'context'     => array( 'view' ),
                'readonly'    => true,
            ),
            'baslik' => array(
                'description' => 'Makale başlığı.',
                'type'        => 'string',
                'context'     => array( 'view' ),
            ),
            'tarih' => array(
                'description' => 'Yayınlanma tarihi.',
                'type'        => 'string',
                'format'      => 'date-time',
                'context'     => array( 'view' ),
            ),
        ),
    );
}

function mysite_makaleler_getir( WP_REST_Request $request ) {
    $yazlar  = get_posts( array( 'numberposts' => 10, 'post_status' => 'publish' ) );
    $sonuclar = array();

    foreach ( $yazlar as $yazi ) {
        $sonuclar[] = array(
            'id'     => $yazi->ID,
            'baslik' => $yazi->post_title,
            'tarih'  => mysql2date( 'c', $yazi->post_date_gmt ),
        );
    }

    $yanit = new WP_REST_Response( $sonuclar, 200 );
    $yanit->header( 'X-WP-Total', count( $sonuclar ) );

    return $yanit;
}

Hata Ayıklama İpuçları

Geliştirme sürecinde işinize yarayacak birkaç pratik ipucu:

  • WP_DEBUG açın: wp-config.php dosyasında define( 'WP_DEBUG', true ); ve define( 'WP_DEBUG_LOG', true ); ekleyin. PHP hataları /wp-content/debug.log dosyasına düşer.
  • Postman veya Insomnia kullanın: GUI üzerinden istek atmak, özellikle POST/PUT isteklerinde çok daha kolay.
  • rest_pre_dispatch hook’u: İstekleri yakalamak ve loglamak için kullanabilirsiniz.
  • Namespace çakışması: Endpoint’iniz çalışmıyorsa önce namespace ve route’u kontrol edin. wp-json/ altındaki tüm route’ları https://siteniz.com/wp-json/ adresini ziyaret ederek görebilirsiniz, JSON olarak tüm namespace’ler listelenir.
  • Permalink ayarları: Endpoint’leriniz 404 dönüyorsa Ayarlar > Kalıcı Bağlantılar sayfasına girip kaydedin. Rewrite kuralları yenilenir.

Cache Stratejisi

Sık erişilen endpoint’lerde veritabanı sorgularınızı cache’lemek performans açısından kritik:

function mysite_cache_li_veri_getir( WP_REST_Request $request ) {
    $cache_key  = 'mysite_ozel_veri_v1';
    $cache_suresi = 5 * MINUTE_IN_SECONDS;

    $veri = get_transient( $cache_key );

    if ( false === $veri ) {
        global $wpdb;
        $veri = $wpdb->get_results(
            "SELECT ID, post_title, post_date FROM {$wpdb->posts}
             WHERE post_status = 'publish' AND post_type = 'post'
             ORDER BY post_date DESC LIMIT 20"
        );

        set_transient( $cache_key, $veri, $cache_suresi );
    }

    $yanit = new WP_REST_Response( $veri, 200 );
    $yanit->header( 'Cache-Control', 'public, max-age=300' );
    $yanit->header( 'X-Cache', false === get_transient( $cache_key ) ? 'MISS' : 'HIT' );

    return $yanit;
}

Cache’i temizlemek için ilgili post güncellendiğinde save_post hook’u içinde delete_transient() çağırabilirsiniz.

Sonuç

WordPress REST API’ye özel endpoint eklemek göründüğü kadar karmaşık değil. register_rest_route() fonksiyonu etrafında dönen bu yapıyı kavradıktan sonra neredeyse sınırsız sayıda senaryo için endpoint yazabilirsiniz.

Önemli noktalara bir kez daha değinelim:

  • permission_callback asla atlama: Güvenlik açısından kritik. __return_true bilinçli bir tercih olmalı, ihmal değil.
  • Sanitization ve validation her zaman: Kullanıcıdan gelen her veriyi sanitize edin, validate edin. Bunu args içinde tanımlamak en temiz yol.
  • Anlamlı hata kodları döndür: 404, 409, 429, 500… Doğru HTTP durum kodları API’nizin kullanılabilirliğini artırır.
  • Namespace versiyonlama: v1, v2 gibi versiyonlama yapısı, gelecekteki değişikliklerde geriye dönük uyumluluğu korur.
  • Cache’i ihmal etme: Özellikle yoğun ziyaret alan sitelerde cache olmadan REST API endpoint’leri site performansını çökertebilir.

Bu yazıda anlattıklarını functions.php dosyanıza eklemek işe yarar, ama uzun vadede bu kodları bir eklentiye taşımanızı öneririm. Tema değiştiğinde veya functions.php güncellendiğinde endpoint’leriniz kaybolmasın.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir