One controversial design decision in the WordPress REST API was to have a separate route for each post type. Some people disagreed as combining multiple post types in one route would have been very useful. As a result, you can’t query for posts of the same post type in the same request. This makes sense in terms of following the REST standard, also, the WordPress REST API is super extensible, so you can change that behavior. I’ll show you how in this article.
In this article, I’ll show you how to make the default posts route of the WordPress REST API — wp/v2/posts — work with multiple post types. This is also a practical example of how to customize how the WordPress REST API uses WP_Query and how to declare new arguments for routes.
Filtering WP_Query Arguments For The WordPress REST API
The most important thing to keep in mind when working with the WordPress REST API is that it is an interface on top of the same standard database APIS — WP_Query, WP_Users, WP_Term_Query, etc — that we always use.
When you ask the REST API for posts, it is translating a RESTful API request to WP_Query arguments. If you know WP_Query then you can customize the post routes to fit your needs, if you know the right filters.
If you look at the WP_Rest_Post_Controller you will see the rest_{$this->post_type}_query filter. This filter controls the WP_Query arguments used for GET requests for posts.
WP_Rest_Post_Controller is used for all post types that have REST API routes. At the very least that’s post and page, but could also be any custom post type that is declared with show_in_rest true and doesn’t declare a different controller class.
Since this class is reused for all post type routes, the filter is dynamically named. When using wp/v2/posts it will be rest_post_query. Our goal here is to make that route work with multiple post types so that’s the filter we will use.
This filter is passed two arguments, the first is the WP_Query params and the second is the WP_REST_Request argument. We can use that object to check for a parameter for post types. Let’s use the query argument “type”. So if you had a post type called “book” and you wanted to use /wp/v2/posts?type=book
You would use the get_param() method of that WP_REST_Request object to check for the type argument:
<?php /** @var WP_REST_Request $request */ $type = $request->get_param( 'type' );
If we use that inside of a callback hooked top rest_post_query, we can use it to modify the post_type argument for WP_Query. I do think it is smart to check if each of the post types specified are registered with show_in_rest true, or you could easily end up leaking sensitive information, for example, if a post type represented a payment.
Here is the complete callback that checks if each post type is registered and marked as available to be shown in the REST API:
Note that I’m adding the default post type to whatever post types are queried for with $post_types[] = $args[ ‘post_type’ ]; That might not be your desired effect. You might want to if types is used to not query for posts in the posts post type at all. You could just remove this line:
Modifying Routes
Technically that’s really all you need. We don’t actually have to register that type argument, but it’s best to add it to the route’s declaration so clients know it is there and so we can have additional validation.
The WP_REST_Server class has rest_endpoints filter that lets you modify the declaration of any endpoints. This filter exposes an array, which is keyed by endpoint. You actually can use that to remove endpoints. For example, you could remove user endpoints like this:
<?php add_action( 'rest_endpoints', function( $endpoints) { foreach ( $endpoints as $endpoint => $args ){ if( false !== strpos( '/wp/v2/users', $endpoint ) ){ unset( $endpoints[ $endpoint ] ); } } return $endpoints; });
In our case, we actually want to add an argument to the /wp/v2/posts endpoint’s GET route. The array we’re working with has to entries in /wp/v2/posts one for the GET route and one for the POST route. We only want to modify the GET route:
<?php add_action( 'rest_endpoints', function( $endpoints ){ if( isset( $endpoints[ 'wp/v2/posts' ] ) ){ foreach( $endpoints[ 'wp/v2/posts' ] as &$post_endpoint ){ if( 'GET' == $post_endpoint[ 'METHOD' ] ){ $post_endpoint[ 'args' ][ 'type' ] = array( 'description' => 'Post types', 'type' => 'array', 'required' => false, 'default' => 'post' ); } } } return $endpoints; }, 15 );
Other Approaches
Using the rest_post_query filter is the easiest way to modify the arguments for the WP_Query object used to build REST API responses. We could have also changed the controller class used for posts using the register_post_type_args filter.
Since this is a common need, I took the code from this article and packaged it into a plugin. You can get the plugin here. But I hope you’ll also use this article as a way to understand how the API works and how to customize how it works with WP_Query.
1 Comment