Crafting Secure Custom REST Endpoints for WordPress: An Approved Review Example
Developing for WordPress often means pushing beyond its excellent content management features into full-blown application territory. When you need to expose highly specific, filtered data to external services or a decoupled frontend, the default WordPress REST API, while powerful, might not offer the exact granularity or authentication mechanism you require out of the box. This is where designing your own custom REST endpoints becomes not just a convenience, but a necessity for robust infrastructure engineering.
Consider a scenario where we have a custom post type for "Product Reviews," and a third-party analytics service needs to consume only the *approved* reviews. Furthermore, this service must authenticate using a simple API key, not a session cookie. Our goal is to build a dedicated, secure endpoint for this purpose.
First, we register our custom route. This should typically be done within a plugin to ensure portability and separation of concerns. We’ll use the `rest_api_init` action hook.
```php add_action( 'rest_api_init', function () { register_rest_route( 'my-plugin/v1', '/approved-reviews', array( 'methods' => 'GET', 'callback' => 'my_plugin_get_approved_reviews', 'permission_callback' => 'my_plugin_api_key_check', 'args' => array( 'posts_per_page' => array( 'default' => 10, 'sanitize_callback' => 'absint', ), 'offset' => array( 'sanitize_callback' => 'absint', ), ), ) ); } ); ```
Here, `my-plugin/v1` defines our namespace and versioning, preventing conflicts and offering flexibility for future API changes. The `/approved-reviews` is our specific route. We’ve set `GET` as the allowed method and assigned a `callback` function for data retrieval and a `permission_callback` for authentication. Notice the `args` array; this is crucial for defining expected parameters, their defaults, and sanitization rules, making your API more predictable and secure from malformed inputs.
Next, we implement the `callback` function, `my_plugin_get_approved_reviews`. This is where we’ll fetch our approved reviews. We’ll assume 'review_status' is a custom field (post meta) for our 'product_review' custom post type.
```php function my_plugin_get_approved_reviews( WP_REST_Request $request ) { $args = array( 'post_type' => 'product_review', 'post_status' => 'publish', 'posts_per_page' => $request->get_param( 'posts_per_page' ), 'offset' => $request->get_param( 'offset' ), 'meta_query' => array( array( 'key' => 'review_status', 'value' => 'approved', 'compare' => '=', ), ), );
$reviews = new WP_Query( $args ); $data = array();
if ( $reviews->have_posts() ) { while ( $reviews->have_posts() ) { $reviews->the_post(); $data[] = array( 'id' => get_the_ID(), 'title' => get_the_title(), 'content' => get_the_content(), 'author' => get_the_author(), 'rating' => get_post_meta( get_the_ID(), 'review_rating', true ), // Assuming another meta field 'date_gmt' => get_the_date( 'c', get_the_ID() ), ); } wp_reset_postdata(); }
return new WP_REST_Response( $data, 200 ); } ```
This function utilizes `WP_Query` with a `meta_query` to filter for approved reviews, extracts relevant data, and returns it as a `WP_REST_Response` with a 200 status code. The parameters `posts_per_page` and `offset` are safely retrieved from the request object, leveraging the sanitization defined in `register_rest_route`.
Finally, the critical `permission_callback`: `my_plugin_api_key_check`. For external services, relying on cookie authentication isn't practical. A shared API key, passed via a custom HTTP header, is a common and effective approach.
```php function my_plugin_api_key_check( WP_REST_Request $request ) { $api_key = $request->get_header( 'X-API-Key' ); // Expecting key in 'X-API-Key' header $expected_key = get_option( 'my_plugin_analytics_api_key' ); // Stored securely in wp_options
if ( empty( $expected_key ) ) { error_log( 'My Plugin API: Expected API key not set in options.' ); return new WP_Error( 'rest_api_key_not_configured', 'API key not configured.', array( 'status' => 500 ) ); }
if ( $api_key && $api_key === $expected_key ) { return true; // Access granted }
return new WP_Error( 'rest_forbidden', 'Invalid API key.', array( 'status' => 401 ) ); } ```
Here, we retrieve the API key from the `X-API-Key` HTTP header. The expected key should be stored securely, perhaps in the `wp_options` table, managed via an admin interface, or even as an environment variable in a more sophisticated setup. If the keys match, the callback returns `true`, granting access. Otherwise, it returns a `WP_Error` object, which automatically translates into a structured error response with a 401 Unauthorized status.
By combining `register_rest_route` with custom callback and permission functions, we gain fine-grained control over what data is exposed, how it's filtered, and who can access it. This approach provides a robust, extensible foundation for integrating WordPress into complex backend architectures and securely serving tailored data to diverse applications.