Adding Custom Routes to the WordPress REST API

Originally published at: https://www.sitepoint.com/wordpress-rest-api-adding-custom-routes/

This article on custom routes was originally published by Torque Magazine, and is reproduced here with permission.

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[\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 sanitize_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.”

This topic was automatically closed 91 days after the last reply. New replies are no longer allowed.