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

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:

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
            '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() {
    .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
        } );
    } )
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() {

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();
        // 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
            thought: { title: responseData.title.rendered, content: responseData.plaintext }

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.

  • MarioFromBelgium

    Hi Jeff,

    again a great tutorial. Haven’t actually tried the code yet, just read you comments…I think I get it!

    I have one question slightly out of this scope. At the end you write “WP-API makes it this easy to expose new data” …I agree 100% that this is great and offers interesting possibilities. I wonder though what to do with “protected” data. I have data that is open for the public but also restricted data only open for registered users.

    All articles I read so far are about how open rest-api is and that it makes data easily accessible. I haven’t seen any explanation on protecting data like posts for non-authenticated visitors.
    I would very much appreciate learning your thoughts/comments on this.
    Best would be a tutorial of course!

    • Hi Mario, when creating custom routes or adding fields to existing routes, you’ll need to protect any data that needs it using WordPress’ built in functions like `current_user_can()` – but all of the routes that come built in to WP-API should already respect WordPress’ permissions, so a query to wp/v2/posts/ would only return public posts to unauthenticated users, but could return private posts if the user was authenticated.

      If you’re using a web-based interface, authentication is actually pretty easy: you can just have the user log in to WordPress via wp-login.php and then they will be authenticated using cookies. On the other hand, if you’re creating an app like we’re doing in this series, the only good way to authenticate users is by using OAuth. So far I haven’t covered this because OAuth is pretty tricky, and doubly so when using JavaScript like we are with the REACT app since JavaScript doesn’t have built-in encryption functions.

      I’m planning to cover authentication in this app in a future installment. In the meantime you can read more about authentication with WP-API in their documentation here: http://v2.wp-api.org/guide/authentication/

      • MarioFromBelgium

        Hi Jeff,

        Looking forward to read your work on authentication.

        In the mean time I can only partially agree with what you wrote on protected posts(and others). You are right that when posts are protected by WP or a plugin(I’m Using S2Member) WP-REST-API will also not transfer that data….that is only when you follow the top-route like : http://www.domain.com/wp-json/wp/v2/posts

        If you add the id-number of a protected post like http://www.domain.com/wp-json/wp/v2/posts/1 it will give you the data. It has done so in v1 and it does so in v2.

        I have reported this to the WP-REST-API team but with little or no return/support.

        Again I’m very exited about all this but I have the feeling that security is not the first focus of the development team….which could be the (only) reason why not to go the wp-rest-api way when developing a wordpress based webservice.


  • shankiesan

    Hey guys, great article. We’re big fans of Migrate DB Pro and you guys always seem to be a couple months ahead of us in your projects. So surprise surprise, we’re now building a React app with a WP back end.

    One problem that I’ve been having is when adding query parameters to an API request (which you don’t have above as your request is to return ‘all’ post IDs). For some reason, when I try to register a rout as they do in the docs, such as:

    `add_action( ‘rest_api_init’, function () {

    register_rest_route( ‘myplugin/v1’, ‘/author/(?Pd+)’, array(
    ‘methods’ => ‘GET’,
    ‘callback’ => ‘my_awesome_func’,
    ) );
    } );`

    it fails to register the route – it doesn’t appear in the index (i.e. …/myplugin/v1/), and it 404s if you attempt to go to …/myplugin/v1/author/5

    Have you guys come across this?


    • Hey, sorry to have missed this. You’ve probably got this figured out by now, but my first instinct would be to make sure that you flush your rewrite rules by clicking the “Save Changes” button on wp-admin->settings->permalinks

      • shankiesan

        Thanks Jeff! I had flushed rewrite rules… turns out that wasn’t the problem. For anyone else who has this problem, it turns out that you need register_rest_route for both the parent and the query, i.e. in this case, register one route for /author and then a second for /author/(?Pd+). Took us ages to figure out!

  • Antifaith

    Would it be difficult to change this into a web app to run in the browser?

    • Probably wouldn’t be too difficult – in fact, you could probably reuse most of this code if you were using React on the web.

  • sba7elfol

    Thank you so much! My dream was to get started so quickly into this world is becoming true thanks to your tutorials 🙂

    On a side note, I got stuck for few minutes due to a small mistake:

    “Now you can activate your plugin and see that requests to wp-json/wp/v2/post/[…] ..”
    In the link, it should be “posts” rather than “post”.

    Please continue with this series 🙂

    Regards from Egypt

  • Sba7elfol

    Nicely Done!

    Please remove the extra dot from:
    thought: { title: responseData[0].title.rendered, content: responseData[0].plaintext }

    Please continue this series 🙂

  • rohit rajput

    how to create api in wordress folder

  • svrooij

    Even though they are recommending against removing fields, I create a plugin that can do server-side filtering of the amount of properties being returned. With https://wordpress.org/plugins/rest-api-filter-fields/ you can use the `fields` to specify the fields you want back. This can even improve your react app further.

  • rotexhawk

    I am able to expose custom fields on a custom post type but when I make a post request, wordpress doesn’t recognize it. The custom fields are added through ACF plugin. Can’t find anything related to customizing wordpress post parameters. Thank u.

  • Mateusz Mysiak

    I seem to be missing something. I’ve tried this tutorial and another one from here https://code.tutsplus.com/tutorials/wp-rest-api-internals-and-customization–cms-24945 . I’ve noticed some differences, so I’ve already tried modifying both scripts, but no combination would result in the custom fields showing in the get response. Any help, please?

    ***Edit*** Hours of fiddling didn’t help. It’s always just after asking a question that the answer appears. For anyone with the same problem, I just needed to activate the plugin in the wordpress admin panel…

  • Bharat

    The register_api_field is renamed to register_rest_field. Looks like its no longer backwards compatible as of WordPress 4.8. Need to update this article to reflect the same.