WP REST API Part 2: Customizing Default Endpoints and Adding New Ones

#
By Jeff Gould
JSON response showing new plaintext field

In the first installment of this series I created a very basic React Native app utilizing WordPress and the WP JSON API. In this article, I’ll pick up where the app left off and add a few customizations to the API that will enable some more features in the app.

If you’re going to follow along with this article, make sure that you’re caught up with the first installment because we’re going to hit the ground running!

Cleaning Up

Originally I added a filter to the active theme’s functions.php file in order to add a plaintext node to the content field:

add_filter( 'rest_prepare_post', 'dt_use_raw_post_content', 10, 3 );
function dt_use_raw_post_content( $data, $post, $request ) {
    $data->data['content']['plaintext'] = $post->post_content;
    return $data;
}

Since that article was published, I’ve been informed that this wasn’t a great way to do things and the v2 documentation shares that sentiment:

[…] it’s important to keep in mind that the API is about exposing an interface to all clients, not just the feature you’re working on. Changing responses is dangerous. Adding fields is not dangerous, so if you need to modify data, it’s much better to duplicate the field instead with your modified data.

So the first thing I’m going to do is delete those lines from functions.php. Since React Native doesn’t seem to have a good way to deal with html and html entities out of the box, we’re still going to need to expose a plaintext version of our post content but we’re also going to be adding some more code that’s specific to our Deep Thoughts app a bit later, so we might as well put all of our code into a plugin.

Create a folder in your site’s plugins directory called deep-thoughts-plugin and then create a php file inside that called deep-thoughts-plugin.php with the following bit of boilerplate:

<?php
/*
Plugin Name: Deep Thoughts Functionality
Description: API Modifications for my Deep Thoughts React Native app.
Author: Jeffrey Gould
Version: 1.0
Author URI: http://jrgould.com
*/

if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly

Adding Fields to Existing Endpoints

Now we can add our plaintext field back into the post endpoint, this time using the register_api_field function provided by WP-API. The WP-API docs recommend that new fields be registered at the rest_api_init action, so we’ll create a function that contains our field registration and pass it to that action and another function that will actually return the plaintext content.

add_action( 'rest_api_init', 'dt_register_api_hooks' );
function dt_register_api_hooks() {

    // Add the plaintext content to GET requests for individual posts
    register_api_field(
        'post',
        'plaintext',
        array(
            'get_callback'    => 'dt_return_plaintext_content',
        )
    );
}

// Return plaintext content for posts
function dt_return_plaintext_content( $object, $field_name, $request ) {
    return strip_tags( html_entity_decode( $object['content']['rendered'] ) );
}

Here we’ve use register_api_field to add a field called plaintext to the post endpoint which will receive its content from the callback function that we provided, dt_return_plaintext_content for get requests only. Check out the WP-API Docs for more info on the register_api_field function.

Now you can activate your plugin and see that requests to wp-json/wp/v2/post/[...] now include the plaintext field:

JSON response showing new plaintext field

We’ll also need to update our React Native app to reference this field in the fetchData method that we created by setting responseData[0].plaintext instead of responseData[0].content.plaintext in our call to setState:

this.setState( {
    thought: { title: responseData[0].title, content: responseData[0]..plaintext }
} );` 

Adding Custom Endpoints

With WP-API, adding custom endpoints is just as easy as adding custom fields to existing endpoints. WP-API has got some great documentation for this, so I won’t go into too much detail about the ins and outs, let’s just add a custom endpoint.

Currently, our React Native app is making a request to http://deep-thoughts.dev/wp-json/wp/v2/posts/?filter[orderby]=rand&filter[per_page]=1 which will return an array containing a single random post. This works well but, as I mentioned in the previous article, it’s not very performant. In general WordPress queries using orderby=rand are pretty slow, and on top of that we can’t cache requests to this url since it needs to return something different every time. The ideal solution is to add a custom API endpoint that will return a list of all of the post IDs to our React Native app, then we can select a random post ID and send a direct request for that post every time we want to display a new random post in the app.

First we’ll use the register_rest_route function to register a new route at wp-json/deep-thoughts/v1/get-all-post-ids. We’ll add this to the dt_register_api_hooks function that we created earlier:

// Add deep-thoughts/v1/get-all-post-ids route
register_rest_route( 'deep-thoughts/v1', '/get-all-post-ids/', array(
    'methods' => 'GET',
    'callback' => 'dt_get_all_post_ids',
) );

Now we can add the dt_get_all_post_ids callback function to the main plugin scope that will return an array of all post IDs. We’ll store the results of this query in a transient that expires every 2 hours so that WordPress doesn’t have to query the entire wp_posts table every time someone requests this data:

// Return all post IDs
function dt_get_all_post_ids() {
    if ( false === ( $all_post_ids = get_transient( 'dt_all_post_ids' ) ) ) {
        $all_post_ids = get_posts( array(
            'numberposts' => -1,
            'post_type'   => 'post',
            'fields'      => 'ids',
        ) );
        // cache for 2 hours
        set_transient( 'dt_all_post_ids', $all_post_ids, 60*60*2 );
    }

    return $all_post_ids;
}

You can test if this is working by visiting the new endpoint, http://deep-thoughts.dev/wp-json/deep-thoughts/v1/get-all-post-ids, in your browser. You should get a response that looks like this:

  [  150,  148,  147,  146,  145,  144,  143,  142,  141,  140,  139,  138,
     137,  136,  135,  134,  133,  132,  131,  130,  129,  128,  127,  126,
     125,  124,  123,  122,  121,  120,  119,  118,  117,  116,  115,  114,
     113,  112,  111,  110,  109,  108,  107,  106,  105,  104,  103,  102,
     101,  100,  99,  98  ]

Now we just need to update our react native app to utilize this new endpoint and make cacheable requests. First we’ll replace the REQUEST_URL variable with REQUEST_URL_BASE and add two more variables for the different endpoints we’ll be accessing:

var REQUEST_URL_BASE  = 'http://deep-thoughts.dev/wp-json/';
var POSTS_URL_PATH    = 'wp/v2/posts/';
var GET_POST_IDS_PATH = 'deep-thoughts/v1/get-all-post-ids';

Then we’ll need to add two new properties to the initial state of the app: thoughtIDs and currentID:

getInitialState: function() {
    return {
        //thought is initially set to null so that the loading message shows
        thought: null,
        thoughtIDs: null,
        currentID: null
    };
}

Next, we’ll add methods to populate those properties:

getAllIDs: function() {
    fetch(REQUEST_URL_BASE + GET_POST_IDS_PATH)
    .then((response) => response.json())
    .then((responseData) => {
        // this.setState() will cause the new data to be applied to the UI that is created by the `render` function below
        this.setState( {
            thoughtIDs: responseData
        } );
    } )
    .then(this.fetchData)
    .done();
},
getRandID: function() {
    var currentID = this.state.thoughtIDs[Math.floor(Math.random()*this.state.thoughtIDs.length)];
    if ( this.state.currentID == currentID ) {
        currentID = this.getRandID();
    } else {
        this.setState( {
            currentID: currentID
        } );
    }
    return currentID;
}

You’ll notice that the last thing getAllIDs() does after it has retrieved the list of IDs and added them to the state is that it calls this.fetchData() – this is because fetchData() will need the thoughtIDs to already be available in the state before it can run. We’ll need to replace fetchData() with getAllIDs() in the componentDidMount() method to make sure that getAllIDs() runs first:

// Automatically called by react when this component has finished mounting.
componentDidMount: function() {
    this.getAllIDs();
}

And then we’ll need to update fetchData() to run getRandID() and then use the random ID to fetch that post:

fetchData: function() {
    var currentID = this.getRandID();
    this.setState({
        // we'll also set thought to null when loading new thoughts so that the loading message shows
        thought: null,
    });
    fetch(REQUEST_URL_BASE + POSTS_URL_PATH + currentID)
    .then((response) => response.json())
    .then((responseData) => {
        // this.setState() will cause the new data to be applied to the UI that is created by the `render` function below
        this.setState({
            thought: { title: responseData.title.rendered, content: responseData.plaintext }
        });
    })
    .done();
}

Not much has changed in fetchData(), we’ve added the call to getRandID() at the beginning and then we’re using the output of that along with REQUEST_URL_BASE and POSTS_URL_PATH to access a specific post via the API with a url that is easily cacheable by a server-side caching system such as WP Super Cache or Varnish.

Here’s a gist of the entire index.ios.js file so far:

And, while we’re at it, here’s our Deep Thoughts WordPress plugin so far:

Endpoints Are Just The Beginning

As you can see, WP-API makes adding custom fields and endpoints a relatively trivial task. Custom API endpoints often make much more sense than using admin-ajax and are a bit easier to deal with, in my opinion.

At the moment, our app isn’t much more than it was when you started reading this article, but there’s a lot of potential now that we’ve unlocked the power of custom routes. If WP-API makes it this easy to expose new data, then it’s probably just as easy to expose new ways to interact with our data. That’s an exciting prospect, and it’s exactly what we’ll be exploring next time!

About the Author

Jeff Gould

Jeff is a problem solver at heart who found his way to the web at an early age and never looked back. Before Delicious Brains, Jeff was a freelance web developer specializing in WordPress and front-end development.