Most of the discussion around the WordPress REST API has been about querying the default routes. In that sense, we’re treating it as a monolithic API—like the Twitter API, for example.
The truth is, however, that the WordPress REST API is not one API, but millions of highly customizable APIs, which can also be leveraged as a tool for making APIs. Yes, it comes with default routes, but, by necessity, those routes are a compromise between tens of millions of sites, including many that haven’t been made yet.
Just like WordPress isn’t just the global WP_Query object, the REST API isn’t just the default API. Sticking to defaults is like having a traditional WordPress project without ever creating your own WP_Query object, or overwriting the default query at pre_get_posts. It’s possible, but not every job can be done with the default WordPress URL routes alone.
The same is true with the REST API.
In a recent interview with the REST API’s co-lead developer Ryan McCue, he talked about how version two of the project is split into two parts—default routes and the infrastructure for creating RESTful APIs. The default routes provide great examples of how to create your own routes.
The system used for adding these routes and endpoints is incredibly well done. I’ll be showing you the basics of how to use it in this article; and, as an example, I’ll demonstrate how to create a custom route with two endpoints that show information about products in an eCommerce site powered by Easy Digital Downloads (EDD). This example is based on an API add-on that I built for my own site. If you want to see the full source on Github, or the API in action, you can.
Although EDD does provide its own RESTful API, I wanted to expose the specific custom fields that I use on my own site. In my own implementation I also incorporate a second route called “docs,” which is wrapped around a custom post type that I use for documentation.
I might have been able to wrangle the EDD API or the core API’s custom post type and meta routes to do what I wanted, but for simplicity (and to have something that had exactly what I needed) I made my own routes and endpoints. It was quick, fun, and worked out great for the two places I’ve implemented it so far.
Adding Routes
Meet My New Favorite Function
Version two of the REST API introduces a new function called register_rest_route(). This lets you add a route to the REST API and pass in an array of endpoints. For each endpoint, you don’t just provide a callback function for responding to the request, but you can also define what fields you want in your query—which includes defaults, sanitation and validation callbacks, as well as a separate permissions callback.
There are additional features here that are still evolving, I recommend reading the class for the default posts routes. It is a great resource on how to use the REST API to query posts.
I’m going to focus on these three things: callback, field arguments, and permissions check. These three functions will illustrate how the architecture of the API works. They’re also really useful because once you get to your callback, you will have all of the fields you need, and they will be sanitized. You will also know that the request is authorized.
This architecture enforces separation of concerns and helps keep your code modular. I can’t overstate how much I love it.
Setting Up the Route
When defining a custom route, use the register_rest_route() in a function hooked to “rest_api_init,” which is the action that runs when the REST API is initialized. It’s an important action that will likely be as valuable as “plugin_loaded” and “init.”
This function accepts four arguments:
The first is the namespace for the route. All routes must be namespaced, which is then used as the next URL segment after “wp-json.” The default routes are namespaced with wp. This means that the core routes have URLs like “wp-json/wp/posts” while a custom route “sizes” in the namespace “happy-hats-store” would have the url “wp-json/happy-hats-store/sizes.”
These namespaces act like PHP namespaces, or unique slugs for a plugin’s functions. They avoid clashes between routes. That way, if I write a plugin that adds a route called menus, it can be used side by side with a plugin you wrote that adds a route called menus—just as long as we use different namespaces that correspond to the plugin’s name. Namespaces for routes are a smart system since it’s very likely that two or more developers will add routes with the same name.
The second argument is the URL after the namespace for your route. In this example, my first route is “/products” and the second is “/products’ . ‘/(?P<id>[\d]+).” The second route allows for a number, for example a Post ID in the last URL segment. These route URLs get joined to the namespace. So, if your namespace is “chewbacca-api” and your route is “/products,” then the URL for it will be “/wp-json/chewbacca-api/products.”
register_rest_route( 'chewbacca-api', '/products', array() );
It’s good practice to include a version number in your namespaces. I used calderawp_api/v2 for my namespace.
The third argument is where the real magic happens. It is where you add endpoints to a route. That’s what the rest of this article is about, so we will skip it for a second.
The fourth and last argument is an optional boolean argument, called “override.” It is there to help deal with clashes that may occur intentionally or unintentionally with already defined routes. By default, this argument is false, and when it is false, an attempt will be made to merge routes. You can optionally set this to true to replace already declared routes.
Setting Up Your Endpoints
So far we talked about setting up routes, but routes are only useful if they have endpoints. For the rest of this article we will talk about adding endpoints to the route using the third argument of register_rest_route().
Transport Method
All endpoints need to define one or more HTTP transport methods (GET/POST/PUT/DELETE). By defining an endpoint as only working via GET requests, you are telling the REST API where to get the correct data and how to create errors for invalid requests.
In the array that defines your endpoint, you define your transport methods in a key called “methods.” The class WP_REST_Server provides constants for defining transport methods and types of JSON bodies to request. For example, here is how we would define an endpoint that allows for GET requests only:
register_rest_route( 'chewbacca-api', '/products', array( 'methods' => WP_REST_Server::READABLE, ) );
And here is how we would add a route that accepts all transport methods:
register_rest_route( 'chewbacca-api', '/products', array( 'methods' => WP_REST_Server::ALLMETHODS, ) );
Using these constants, which you can see all of here, ensures that as the REST server evolves, your routes are properly set up for it.
Defining Your Fields
One of the really great parts of the way that endpoints are defined is that you specify the fields you want: what their defaults are and how to sanitize them. This allows the callback function for processing the request to actually trust the data it is retrieving.
The REST API handles that all for you.
Here is an example of how I set up the fields main endpoint that returns a collection of products:
register_rest_route( "{$root}/{$version}", '/products', array( array( 'methods' => \WP_REST_Server::READABLE, 'callback' => array( $cb_class, 'get_items' ), 'args' => array( 'per_page' => array( 'default' => 10, 'sanitize_callback' => 'absint', ), 'page' => array( 'default' => 1, 'sanitize_callback' => 'absint', ), 'soon' => array( 'default' => 0, 'sanitize_callback' => 'absint', ), 'slug' => array( 'default' => false, 'sanitize_callback' => 'sanitize_title', ) ), 'permission_callback' => array( $this, 'permissions_check' ) ), ) );
You will notice that most of these are number or boolean fields, so I set them up to be sanitized using absint(). There is one field for querying by post slug. I used santize_title for it since it is the same way they are sanitized before being written to the database.
My other route is for showing a product by ID. In that route’s endpoint I didn’t specify any fields because the ID passed in the last URL segment is enough.
register_rest_route( "{$root}/{$version}", '/products' . '/(?P<id>[\d]+)', array( array( 'methods' => \WP_REST_Server::READABLE, 'callback' => array( $cb_class, 'get_item' ), 'args' => array( ), 'permission_callback' => array( $this, 'permissions_check' ) ), ) );
You can use these examples to craft your own routes. Just keep in mind that my examples are written in object context—i.e., they are going to be used inside a method of class. Also, that method needs to be hooked to “rest_api_init.”
The Callback Function
The callback function, which you specify for each route in the key “callback,” is the method that the request will be dispatched to, if the permissions callback passes.
In my example, I’m passing my main route to a method of the callback class called “get_items” and the single product route to a method called “get_item.” This follows the conventions set out in the core post query class. That’s important because my callback class actually extends that class in the core API “WP_REST_Post_Controller.” This allows me to absorb a lot of its functionality while defining my own routes. I will discuss processing requests and responding to them in this function later on. At this stage we are just defining what it is.
In the last section, I showed you two route registrations. Both have an array for “callback” that passes an object of the class used for the callback and the name of the function.
The Permissions Callback
Like the main callback, this method passed an object of the WP_Request_Class, which allows you to use parts of the request for your authentication scheme. The permissions callback just needs to return true or false; how you get there is up to you.
One strategy is to use the traditional check current user capabilities type logic that you’re using to using in WordPress development. This is possible because the permissions check will run after current user is set, so if you are using any of the authentication methods, they will already have run.
You do not have to rely on WordPress’s current user or authentication at all. One thing you can do is add specific authorization for your custom routes and check the specific parts of the request for the right keys. Another option is, if your site implemented social login, you could check for the oAuth keys, authenticate them against that network, and, if they pass, login the user who is associated with that account.
I’ll discuss these strategies more in the future.
For the example in the this article, I am showing how to create a public, read only API, so we can either create one function that always returns true to use as our permissions callback, or use WordPress’s __return_true. I went with the first option, so I’d have it in place for the future when I will start adding authenticated POST requests.
Processing and Responding to Requests
The callback function for each endpoint will be passed an object of the WP_REST_Request class. There is a method for getting all of the data from the request sanitized and validated, with the defaults filled in.
Most of the time, we can just use the method get_params(). This gives us the parameters from the request mapped from whichever transport method we provided. Using this method, instead of accessing the global POST or GET variables, is important for many reasons.
First off, the array that’s returned is validated and sanitized for us. Also, it handles switches between transport methods. That means that if you switch the endpoints definition from using GET to PUT (that’s a one line change), all of the code in the callback will work just fine.
It also leads to better abstraction. I’m showing a basic version of my API add-on plugin in this article, but if you look at the source for the plugin it’s based on, you’ll see that all of the queries for the products and docs’ endpoints are handled by an abstract class that handles creating WP_Query arguments, looping through the results and returning them.
Regardless of how you handle your endpoint processing, you will want to end with an instance of the WP_REST_Response class. The best way to do so is by using the function ensure_rest_response(), which returns an instance of this class, and can also handle errors well.
This class ensures that your response is properly formed JSON and has the minimum needed headers. It also provides methods for adding extra headers.
Here you can see how I used it to add headers, based on how the core post routes add headers for total results, pages and previous/next links:
/** * Create the response. * * @since 0.0.1 * * @access protected * * @param \WP_REST_Request $request Full details about the request * @param array $args WP_Query Args * @param array $data Raw response data * * @return \WP_Error|\WP_HTTP_ResponseInterface|\WP_REST_Response */ protected function create_response( $request, $args, $data ) { $response = rest_ensure_response( $data ); $count_query = new \WP_Query(); unset( $args['paged'] ); $query_result = $count_query->query( $args ); $total_posts = $count_query->found_posts; $response->header( 'X-WP-Total', (int) $total_posts ); $max_pages = ceil( $total_posts / $request['per_page'] ); $response->header( 'X-WP-TotalPages', (int) $max_pages ); if ( $request['page'] > 1 ) { $prev_page = $request['page'] - 1; if ( $prev_page > $max_pages ) { $prev_page = $max_pages; } $prev_link = add_query_arg( 'page', $prev_page, rest_url( $this->base ) ); $response->link_header( 'prev', $prev_link ); } if ( $max_pages > $request['page'] ) { $next_page = $request['page'] + 1; $next_link = add_query_arg( 'page', $next_page, rest_url( $this->base ) ); $response->link_header( 'next', $next_link ); } return $response; }
You’ll notice that I didn’t discuss how to get your data together for the response. It’s up to you how you do so.
You can use WP_Query, wpdb, get_post_meta, or use a plugin’s built in functions. It’s up to you, it’s your API. These are already skills you have as WordPress developer.
In many cases, if you’re adding a RESTful API to an existing plugin or site, you should already have classes for getting the data you need. You can use the REST API to get parameters for those classes from a HTTP request, and then pass the results to the REST API’s response class.
In my API, I used WP_Query to get the posts. Here is the method I used to loop through the WP_Query object and get the data I needed:
/** * Query for products and create response * * @since 0.0.1 * * @access protected * * @param \WP_REST_Request $request Full details about the request * @param array $args WP_Query args. * @param bool $respond. Optional. Whether to create a response, the default, or just return the data. * * @return \WP_HTTP_Response */ protected function do_query( $request, $args, $respond = true) { $posts_query = new \WP_Query(); $query_result = $posts_query->query( $args ); $data = array(); if ( ! empty( $query_result ) ) { foreach ( $query_result as $post ) { $image = get_post_thumbnail_id( $post->ID ); if ( $image ) { $_image = wp_get_attachment_image_src( $image, 'large' ); if ( is_array( $_image ) ) { $image = $_image[0]; } } $data[ $post->ID ] = array( 'name' => $post->post_title, 'link' => get_the_permalink( $post->ID ), 'image_markup' => get_the_post_thumbnail( $post->ID, 'large' ), 'image_src' => $image, 'excerpt' => $post->post_excerpt, 'tagline' => get_post_meta( $post->ID, 'product_tagline', true ), 'prices' => edd_get_variable_prices( $post->ID ), 'slug' => $post->post_name, ); for ( $i = 1; $i <= 3; $i++ ) { foreach( array( 'title', 'text', 'image' ) as $field ) { if ( 'image' != $field ) { $field = "benefit_{$i}_{$field}"; $data[ $post->ID ][ $field ] = get_post_meta( $post->ID, $field, true ); }else{ $field = "benefit_{$i}_{$field}"; $_field = get_post_meta( $post->ID, $field, true ); $url = false; if ( is_array( $_field ) && isset( $_field[ 'ID' ] )) { $img = $_field[ 'ID' ]; $img = wp_get_attachment_image_src( $img, 'large' ); if ( is_array( $img ) ) { $url = $img[0]; } } $_field[ 'image_src' ] = $url; $data[ $post->ID ][ $field ] = $_field; } } } return $data; } } if ( $respond ) { return $this->create_response( $request, $args, $data ); } else { return $data; } }
As you can see, it is a mix of post fields, some meta fields, and functions defined by EDD. Again, if you want to see the full source, including how I added a second route and more endpoints to each of the routes, head over to GitHub and take a look.
Custom APIs, Like Winter, Are Coming
The fact that the WordPress REST API is adding a useful set of default routes to your site is awesome. This is known.
What’s even more exciting, and more awesome, is that in service of creating routes, we are getting a really awesome RESTful API server that gives us powerful tools for creating our own custom APIs.
So, next time you can’t make a default route do exactly what you want, just make your own.
11 Comments