WordPress REST API 2016’dan beri core’un parçası. Frontend’ler (React, mobile app) WP backend’i API olarak consume ediyor. Custom endpoint geliştirmek pratik ama security discipline şart.
Yıllarca WP plugin’leri geliştirdim, REST endpoint’lerinde common mistakes gördüm. Bu yazıda proper implementation’ı anlatacağım.
Basic endpoint
add_action('rest_api_init', function() {
register_rest_route('myapi/v1', '/orders/(?P<id>\d+)', [
'methods' => 'GET',
'callback' => 'get_order',
'permission_callback' => 'can_read_order',
'args' => [
'id' => [
'required' => true,
'validate_callback' => fn($v) => is_numeric($v) && $v > 0,
'sanitize_callback' => 'absint'
]
]
]);
});
function get_order(WP_REST_Request $request): WP_REST_Response {
$id = $request['id'];
$order = get_order_data($id);
return new WP_REST_Response($order, 200);
}
function can_read_order(WP_REST_Request $request): bool {
return current_user_can('read_orders');
}Bu minimum iyi practice. Her endpoint’te 4 element:
methods: HTTP verbcallback: handler functionpermission_callback: authorization checkargs: parameter validation
Authentication methods
WP REST API multiple auth methods destekliyor:
1. Cookie authentication (default). Logged-in WP user, browser cookie. Frontend’in same origin olması gerek.
2. Application passwords. WP 5.6+ built-in. User account’a özel app password üretiyorsun. Basic auth header.
Authorization: Basic base64(username:app_password)Mobile app, third-party integration için kullanılıyor.
3. JWT. Plugin ile. Stateless, bearer token. Modern SPA’lar için popular.
4. OAuth 2.0. Plugin ile. Third-party app authorization.
Benim preference: application passwords most of the time. JWT SPA-specific.
Permission callback patterns
Pattern 1: Public endpoint.
'permission_callback' => '__return_true'Dikkat: Bu genelde yanlış. Unauthenticated user her istediğini görüyor. Sadece gerçekten public data (blog posts, listing).
Pattern 2: Logged-in user.
'permission_callback' => function() {
return is_user_logged_in();
}Minimum auth. Kullanıcı login olmuş.
Pattern 3: Capability check.
'permission_callback' => function() {
return current_user_can('manage_options');
}WP capability system. manage_options admin, edit_posts author+.
Pattern 4: Custom logic.
'permission_callback' => function(WP_REST_Request $request) {
$order_id = $request['id'];
$user_id = get_current_user_id();
return user_owns_order($user_id, $order_id) || current_user_can('manage_orders');
}Spesifik business rule.
Input validation
Every parameter validate + sanitize:
'args' => [
'email' => [
'required' => true,
'validate_callback' => function($value) {
return is_email($value);
},
'sanitize_callback' => 'sanitize_email'
],
'age' => [
'required' => false,
'validate_callback' => function($value) {
return is_numeric($value) && $value >= 0 && $value <= 150;
},
'sanitize_callback' => 'absint',
'default' => null
],
'tags' => [
'required' => false,
'validate_callback' => function($value) {
return is_array($value) && count($value) <= 10;
},
'sanitize_callback' => function($value) {
return array_map('sanitize_text_field', $value);
}
]
]validate_callback: true return veya WP_Error. Invalid ise 400.sanitize_callback: value’yu güvenli hale getir. Output’u callback’e geçecek.
SQL injection prevention
$wpdb->prepare() her zaman:
// GOOD
$results = $wpdb->get_results($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}orders WHERE user_id = %d AND status = %s",
$user_id,
$status
));
// BAD - SQL injection riski
$results = $wpdb->get_results(
"SELECT * FROM {$wpdb->prefix}orders WHERE user_id = $user_id"
);Her user-provided value placeholder’la parametrelize.
Output escaping
Response data kullanıcıya giderken escape:
function get_order(WP_REST_Request $request): WP_REST_Response {
$id = $request['id'];
$order = get_order_raw_data($id);
return new WP_REST_Response([
'id' => (int) $order['id'],
'title' => esc_html($order['title']),
'description' => wp_kses_post($order['description']), // HTML allowed ama sanitized
'url' => esc_url($order['url']),
'email' => sanitize_email($order['email'])
], 200);
}Rate limiting
WP REST API default rate limit yok. Custom implement:
function rate_limit_check(WP_REST_Request $request): bool {
$user_id = get_current_user_id() ?: $_SERVER['REMOTE_ADDR'];
$key = "rate_limit_{$user_id}";
$count = (int) get_transient($key);
if ($count >= 100) { // 100 requests per minute
return false;
}
set_transient($key, $count + 1, 60);
return true;
}
// Permission callback'te check:
'permission_callback' => function($request) {
if (!rate_limit_check($request)) {
return new WP_Error('rate_limit', 'Too many requests', ['status' => 429]);
}
return current_user_can('read');
}Transient WordPress built-in. Production’da Redis recommended.
Error responses
Consistent error format:
function get_order(WP_REST_Request $request): WP_REST_Response {
$id = $request['id'];
$order = get_order_data($id);
if (!$order) {
return new WP_REST_Response([
'code' => 'order_not_found',
'message' => 'Order not found',
'data' => ['status' => 404]
], 404);
}
return new WP_REST_Response($order, 200);
}HTTP status codes doğru:
– 200: success
– 201: created
– 400: bad request (validation)
– 401: unauthenticated
– 403: unauthorized (authenticated ama izni yok)
– 404: not found
– 429: rate limited
– 500: server error
Namespace’ler
register_rest_route('myplugin/v1', '/endpoint', [...]);myplugin your plugin namespace. v1 version. WP convention: slash-separated.
Yeni version için:
register_rest_route('myplugin/v2', '/endpoint', [...]);Old v1 backward compat kalıyor.
Pagination
List endpoint’lerde pagination:
register_rest_route('myapi/v1', '/orders', [
'methods' => 'GET',
'callback' => 'list_orders',
'args' => [
'per_page' => [
'default' => 20,
'validate_callback' => fn($v) => is_numeric($v) && $v > 0 && $v <= 100
],
'page' => [
'default' => 1,
'validate_callback' => fn($v) => is_numeric($v) && $v >= 1
]
]
]);
function list_orders(WP_REST_Request $request) {
$per_page = $request['per_page'];
$page = $request['page'];
$offset = ($page - 1) * $per_page;
$total = get_total_orders();
$orders = get_orders(['limit' => $per_page, 'offset' => $offset]);
$response = new WP_REST_Response($orders, 200);
$response->header('X-Total-Count', $total);
$response->header('X-Total-Pages', ceil($total / $per_page));
return $response;
}Total count header’da. Frontend pagination kurabilecek.
CORS
Frontend different origin’den çağırıyor:
add_action('rest_api_init', function() {
remove_filter('rest_pre_serve_request', 'rest_send_cors_headers');
add_filter('rest_pre_serve_request', function($value) {
$origin = get_http_origin();
$allowed = ['https://app.example.com', 'https://staging.example.com'];
if (in_array($origin, $allowed)) {
header("Access-Control-Allow-Origin: $origin");
header('Access-Control-Allow-Credentials: true');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE');
header('Access-Control-Allow-Headers: Authorization, Content-Type');
}
return $value;
});
}, 15);Wildcard use etme production’da. Specific allowed origins.
Documentation
REST endpoint’leri dokümante:
OpenAPI/Swagger compatible:
register_rest_route('myapi/v1', '/orders/(?P<id>\d+)', [
'methods' => 'GET',
'callback' => 'get_order',
'permission_callback' => 'can_read_order',
'args' => [...],
'schema' => [
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'order',
'type' => 'object',
'properties' => [
'id' => ['type' => 'integer'],
'title' => ['type' => 'string'],
'status' => ['type' => 'string', 'enum' => ['pending', 'completed', 'cancelled']]
]
]
]);Client dev’ler schema’ya göre typed API client oluşturabiliyor.
Testing
PHPUnit ile endpoint test:
class OrderEndpointTest extends WP_UnitTestCase {
public function test_get_order_success() {
$user_id = $this->factory->user->create(['role' => 'administrator']);
wp_set_current_user($user_id);
$request = new WP_REST_Request('GET', '/myapi/v1/orders/1');
$response = rest_do_request($request);
$this->assertEquals(200, $response->get_status());
$this->assertArrayHasKey('id', $response->get_data());
}
public function test_get_order_unauthorized() {
$request = new WP_REST_Request('GET', '/myapi/v1/orders/1');
$response = rest_do_request($request);
$this->assertEquals(401, $response->get_status());
}
}Sonuç
WP REST API custom endpoint yazma: authentication, permission check, input validation, output escaping, rate limiting, consistent error response. Bu 6 konu baseline.
WordPress convention’larına saygı: namespace’ler, capability system, existing security patterns.
Security reactive değil proactive. Her endpoint’te explicit check. Plugin production’a hazır ise test coverage + security audit.