Rest Api REST API WordPress 4.4 introduced the infrastructure for a REST API.  The REST API provides an easy way to get data into and out of WordPress.  Data can be retrieved and stored by sending HTTP requests to the REST API server.  The REST API takes advantage of different HTTP methods. GET should be used for retrieving data from the API. POST should be used for creating new resources (i.e users, posts, taxonomies). PUT should be used for updating resources. DELETE should be used for deleting resources. OPTIONS should be used to provide context about our resources. A resource is any single entity or object.  A good example of a resource for WordPress would be a post. A post has different properties like its title and content.  A response from the API could show us title and content as fields in the response.  The REST API enables us to interact with posts and other WordPress resources  in a new way.  The REST API makes sharing our content with the rest of the web easier, and it provides a structured way to handle complex interactions within WordPress. In this chapter of the Plugin Handbook, we will explore how the API works and how we can leverage its power to do great things with WordPress! Controller Classes Overview When writing endpoints it can be helpful to use a controller class to handle the functionality of an endpoint. Controller classes will provide a standard way to interact with the API and also a more maintainable way to interact with the API. WordPress’s current minimum PHP version is 5.2, if you are developing endpoints that will be used by the WordPress ecosystem at large you should consider supporting WordPress’s minimum requirements. PHP 5.2 does not have namespacing built into it. This means that every function you declare will be in the global scope. If you decide to use a common function name for endpoints like get_items() and another plugin also registers that function, PHP will fail with a fatal error. This is because the function get_items() is being declared twice. By wrapping our endpoints we can avoid these naming conflicts and also have a consistent way to interact with the API. Controllers Controllers typically do one thing; they receive input, and generate output. For the WordPress REST API our controllers will handle request input as WP_REST_Request objects and generate response output as WP_REST_Response objects. Let’s look at an example controller class: class My_REST_Posts_Controller { // Here initialize our namespace and resource name. public function __construct() { $this->namespace = '/my-namespace/v1'; $this->resource_name = 'posts'; } // Register our routes. public function register_routes() { register_rest_route( $this->namespace, '/' . $this->resource_name, array( // Here we register the readable endpoint for collections. array( 'methods' => 'GET', 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), ), // Register our schema callback. 'schema' => array( $this, 'get_item_schema' ), ) ); register_rest_route( $this->namespace, '/' . $this->resource_name . '/(?P[\d]+)', array( // Notice how we are registering multiple endpoints the 'schema' equates to an OPTIONS request. array( 'methods' => 'GET', 'callback' => array( $this, 'get_item' ), 'permission_callback' => array( $this, 'get_item_permissions_check' ), ), // Register our schema callback. 'schema' => array( $this, 'get_item_schema' ), ) ); } /** * Check permissions for the posts. * * @param WP_REST_Request $request Current request. */ public function get_items_permissions_check( $request ) { if ( ! current_user_can( 'read' ) ) { return new WP_Error( 'rest_forbidden', esc_html__( 'You cannot view the post resource.' ), array( 'status' => $this->authorization_status_code() ) ); } return true; } /** * Grabs the five most recent posts and outputs them as a rest response. * * @param WP_REST_Request $request Current request. */ public function get_items( $request ) { $args = array( 'post_per_page' => 5, ); $posts = get_posts( $args ); $data = array(); if ( empty( $posts ) ) { return rest_ensure_response( $data ); } foreach ( $posts as $post ) { $response = $this->prepare_item_for_response( $post, $request ); $data[] = $this->prepare_response_for_collection( $response ); } // Return all of our comment response data. return rest_ensure_response( $data ); } /** * Check permissions for the posts. * * @param WP_REST_Request $request Current request. */ public function get_item_permissions_check( $request ) { if ( ! current_user_can( 'read' ) ) { return new WP_Error( 'rest_forbidden', esc_html__( 'You cannot view the post resource.' ), array( 'status' => $this->authorization_status_code() ) ); } return true; } /** * Grabs the five most recent posts and outputs them as a rest response. * * @param WP_REST_Request $request Current request. */ public function get_item( $request ) { $id = (int) $request['id']; $post = get_post( $id ); if ( empty( $post ) ) { return rest_ensure_response( array() ); } $response = prepare_item_for_response( $post ); // Return all of our post response data. return $response; } /** * Matches the post data to the schema we want. * * @param WP_Post $post The comment object whose response is being prepared. */ public function prepare_item_for_response( $post, $request ) { $post_data = array(); $schema = $this->get_item_schema( $request ); // We are also renaming the fields to more understandable names. if ( isset( $schema['properties']['id'] ) ) { $post_data['id'] = (int) $post->ID; } if ( isset( $schema['properties']['content'] ) ) { $post_data['content'] = apply_filters( 'the_content', $post->post_content, $post ); } return rest_ensure_response( $post_data ); } /** * Prepare a response for inserting into a collection of responses. * * This is copied from WP_REST_Controller class in the WP REST API v2 plugin. * * @param WP_REST_Response $response Response object. * @return array Response data, ready for insertion into collection data. */ public function prepare_response_for_collection( $response ) { if ( ! ( $response instanceof WP_REST_Response ) ) { return $response; } $data = (array) $response->get_data(); $server = rest_get_server(); if ( method_exists( $server, 'get_compact_response_links' ) ) { $links = call_user_func( array( $server, 'get_compact_response_links' ), $response ); } else { $links = call_user_func( array( $server, 'get_response_links' ), $response ); } if ( ! empty( $links ) ) { $data['_links'] = $links; } return $data; } /** * Get our sample schema for a post. * * @param WP_REST_Request $request Current request. */ public function get_item_schema( $request ) { $schema = array( // This tells the spec of JSON Schema we are using which is draft 4. '$schema' => 'http://json-schema.org/draft-04/schema#', // The title property marks the identity of the resource. 'title' => 'post', 'type' => 'object', // In JSON Schema you can specify object properties in the properties attribute. 'properties' => array( 'id' => array( 'description' => esc_html__( 'Unique identifier for the object.', 'my-textdomain' ), 'type' => 'integer', 'context' => array( 'view', 'edit', 'embed' ), 'readonly' => true, ), 'content' => array( 'description' => esc_html__( 'The content for the object.', 'my-textdomain' ), 'type' => 'string', ), ), ); return $schema; } // Sets up the proper HTTP status code for authorization. public function authorization_status_code() { $status = 401; if ( is_user_logged_in() ) { $status = 403; } return $status; } } // Function to register our new routes from the controller. function prefix_register_my_rest_routes() { $controller = new My_REST_Posts_Controller(); $controller->register_routes(); } add_action( 'rest_api_init', 'prefix_register_my_rest_routes' ); Overview & The Future Controller classes tackle two big problems for us while developing endpoints; lack of namespacing and consistent structures. It is important to note that you should not abuse inheritance of your endpoints. For example: if you wrote a controller class for a posts endpoint, like the above example, and wanted to support custom post types as well, you should NOT extend your My_REST_Posts_Controller like this class My_CPT_REST_Controller extends My_REST_Posts_Controller. Instead you should either create an entirely separate controller class or make My_REST_Posts_Controller handle all available post types. When you start go down the dark chasm of inheritance, it is important to understand that if the parent classes ever have to change at any point and your subclasses are dependent on them, you will have a major headache. In most cases, you will want to create a base controller class as either an interface or abstract class, that each of your endpoint controllers can implement or extend. The abstract class approach is being taken by the WP REST API team for the potential inclusion to core for the WP_REST_Controller class. Currently, “core endpoints” supporting posts, post types, post statuses, revisions, taxonomies, terms, users, comments, and attachments/media resources, are being developed in a feature plugin that will hopefully be moved into WordPress core at some point. Within the plugin is a proposed WP_REST_Controller class that can be used to build your own controllers for your endpoints. WP_REST_Controller features a lot of advantages and a consistent way to create endpoints for the API. Requests Overview The REST API is very simple in many ways. There is input, known as the request. The input is interpreted by the server and output is created. The output, is known as the response. In some ways, you can think of a request to the WordPress REST API as a set of directions or instructions that should be carried out and interpreted by the API. By default, the WordPress REST API is intended to use HTTP requests as its request medium. HTTP is the foundation for communication of data over the internet, which makes the WordPress REST API a very far reaching API. Requests in the API utilize a lot of the different aspects present in HTTP requests like URIs, HTTP methods, headers, and parameters. The data structure of a request is conveniently handled by the WP_REST_Request class. WP_REST_Request This class is one of the three main infrastructure classes introduced in WordPress 4.4. When an HTTP request is made to an endpoint of the API, the API will automatically create an instance of the WP_REST_Request class, matching the provided data. The response object is auto-generated in WP_REST_Server‘s serve_request() method. Once the request is created and authentication is checked, the request is dispatched and our endpoint callbacks begin to be fired. All of the data stored up in the WP_REST_Request object is passed into our callbacks for our registered endpoints. So both our permissions_callback and callback are called with the request object being passed in. This enables us to access the various request properties in our callbacks, so that we can tailor our responses to match the desired output. Request Properties Request objects have many different properties, each of which can be used in various ways. The main properties are the request method, route, headers, parameters and attributes. Let’s break each of these down into their role in a request. If you were to create a request object yourself it would look like this: $request = new WP_REST_Request( 'GET', '/my-namespace/v1/examples' ); In the above code sample we are only specifying that the request object method is GET and we should be matching the route /my-namespace/v1/examples which in the context of an entire URL would look like this: https://ourawesomesite.com/wp-json/my-namepsace/v1/examples. The method and route arguments for the WP_REST_Request constructor are used to map the request to the desired endpoint. If the request is made to an endpoint that is not registered then a helpful 404 error message is returned in the response. Let’s look at the various properties in more depth. Method The method property of a request object by default matches the HTTP Request method. The method in most cases will be one of GET, POST, PUT, DELETE, OPTIONS, or HEAD. These methods will be used to match the various endpoints registered to a route. When the API finds a match for the method and route it will fire the callbacks for that endpoint. The following convention is a best practice for matching HTTP methods: GET for read only tasks, POST for creation, PUT for updating, and DELETE for deleting. The request method acts as an indicator for the expected functionality of your endpoints. When you make a GET request to a route, you should expect to be returned read only data. Route The route for a request, by default, will match the server environment variable for path info; $_SERVER['PATH_INFO']. When you make an HTTP request to a route of the WordPress REST API, the generated WP_REST_Request object will be made to match that path, which will hopefully then be matched to a valid endpoint. In short the route for a request is where you want to target your request in the API. If we had registered a books endpoint, using GET, it might live at https://ourawesomesite.com/wp-json/my-namespace/v1/books. If we went to that URL in our browser, we would see our collection of books represented in JSON. WordPress will automatically generate the request object for us and handle all of the routing to match endpoints. So since we don’t really have to worry about the routing ourselves understanding how to pass extra data we want in our requests is a much more important thing to understand. Headers HTTP Request headers are simply just extra data about our HTTP request. Request headers can specify caching policy, what our request content is, where the request is coming from and many other things. Request headers do not necessarily interact with our endpoints directly, but the information in the headers helps WordPress know what to do. To pass in data that we want our endpoints to interact with we want to use parameters. Parameters When making requests to the WordPress REST API, most of the additional data passed in will take on the form of parameters. What are parameters? There are four different types in the context of the API. There are route parameters, query parameters, body parameters, and file parameters. Let’s take a look at each one a bit more in depth. URL Params URL parameters are automatically generated in a WP_REST_Request from the path variables in the requested route. What does that mean? Let’s look at this route, which grabs individual books by id: /my-namespace/v1/books/(?P\d+). The odd looking (?P\d+) is a path variable. The name of the path variable is ‘ id‘. If we were to make a request like GET https://ourawesomesite.com/wp-json/my-namespace/v1/books/5, 5 will become the value for our id path variable. The WP_REST_Request object will automatically take that path variable and store it as a URL parameter. Now inside of our endpoint callbacks we can interact with that URL parameter really easily. Let’s look at an example. // Register our individual books endpoint. function prefix_register_book_route() { register_rest_route( 'my-namespace/v1', '/books/(?P\d+)', array( // Supported methods for this endpoint. WP_REST_Server::READABLE translates to GET. 'methods' => WP_REST_Server::READABLE, // Register the callback for the endpoint. 'callback' => 'prefix_get_book', ) ); } add_action( 'rest_api_init', 'prefix_register_book_route' ); /** * Our registered endpoint callback. Notice how we are passing in $request as an argument. * By default, the WP_REST_Server will pass in the matched request object to our callback. * * @param WP_REST_Request $request The current matched request object. */ function prefix_get_book( $request ) { // Here we are accessing the path variable 'id' from the $request. $book = prefix_get_the_book( $request['id'] ); return rest_ensure_response( $book ); } // A simple function that grabs a book title from our books by ID. function prefix_get_the_book( $id ) { $books = array( 'Design Patterns', 'Clean Code', 'Refactoring', 'Structure and Interpretation of Computer Programs', ); $book = ''; if ( isset( $books[ $id ] ) ) { // Grab the matching book. $book = $books[ $id ]; } else { // Error handling. return new WP_Error( 'rest_not_found', esc_html__( 'The book does not exist', 'my-text-domain' ), array( 'status' => 404 ) ); } return $book; } In the example above we see how path variables are stored as URL parameters in the request object. We can then access those parameters in our endpoint callbacks. The above example is a pretty common use case for using URL params. Adding too many path variables to a route can slow down the matching of routes and it can also over complicate registering endpoints, it is advised to use URL parameters sparingly. If we aren’t supposed to use parameters directly in our URL path, then we need another way to pass in extra information to our request. This is where query and body parameters come in, they will typically do most of the heavy lifting in your API. Query Params Query parameters exist in the query string portion of a URI. The query string portion of a URI in https://ourawesomesite.com/wp-json/my-namespace/v1/books?per_page=2&genre=fiction is ?per_page=2&genre=fiction. The query string is started by the ‘ ?‘ character, the different values within the query string are separated by the ‘ &‘ character. We specified two parameters in our query string; per_page and fiction. In our endpoint we would want to grab only two books from the fiction genre. We could access those values in a callback like this: $request['per_page'], and $request['genre'] ( assuming $request is the name of the argument we are using ). If you are familiar with PHP you have probably used query parameters in your web applications. In PHP, the query parameters get stored in the superglobal $_GET. It is important to note that you should never directly access any superglobals or server variables in your endpoints. It is always best to work with what is provided by the WP_REST_Request class. Another common method for passing in variables to an endpoint is to use body parameters. Body Params Body parameters are key value pairs that are stored in the request body. If you have ever sent a POST request via a , through cURL, or some other method, then you have used body parameters. With body parameters you can pass them as different content types as well. The default Content-Type header for a POST request is x-www-form-urlencoded. When using x-www-form-urlencoded, the parameters are sent like a query string; per_page=2&genre=fiction. An HTML form, by default, will bundle up the various inputs and send a POST request matching the x-www-form-urlencoded pattern. It is important to note that although the HTTP specification does not prohibit the use of sending body parameters in GET requests, it is encouraged that you do not use body parameters in a GET request. Body parameters can and should be used for POST, PUT, and DELETE requests. File Params File parameters in a WP_REST_Request object are stored when the request uses a special content type header; multipart/form-data. The file data can then be accessed from the request object using $request->get_file_params(). The file parameters are equivalent to the PHP superglobal: $_FILES. Remember, do not access the superglobals directly only use what the WP_REST_Request object provides. In the endpoint callback we could use wp_handle_upload() to then add in the desired files to WordPress’s media uploads directory. The file parameters are only useful for dealing with file data and you should never use them for any other purpose. Attributes WP_REST_Request also supports request attributes. The attributes of a request are the attributes registered to the match route. If we made a GET request to my-namespace/v1/books, and then we called $request->get_attributes() inside of our endpoint callback, we would be returned all of the registration options for the my-namespace/v1/books endpoint. If we made a POST request to the same route and our endpoint callback also returned $request->get_attributes(), we would receive a different set of endpoint options registered to the POST endpoint callback. In the attributes we will get a response containing supported methods, options, whether to show this endpoint in the index, a list of registered arguments for the endpoint, and our registered callbacks. It might look something like this: { "methods": { "GET": true }, "accept_json": false, "accept_raw": false, "show_in_index": true, "args": { "context": { "description": "Scope under which the request is made; determines fields present in response.", "type": "string", "sanitize_callback": "sanitize_key", "validate_callback": "rest_validate_request_arg", "enum": [ "view", "embed", "edit" ], "default": "view" }, "page": { "description": "Current page of the collection.", "type": "integer", "default": 1, "sanitize_callback": "absint", "validate_callback": "rest_validate_request_arg", "minimum": 1 }, "per_page": { "description": "Maximum number of items to be returned in result set.", "type": "integer", "default": 10, "minimum": 1, "maximum": 100, "sanitize_callback": "absint", "validate_callback": "rest_validate_request_arg" }, "search": { "description": "Limit results to those matching a string.", "type": "string", "sanitize_callback": "sanitize_text_field", "validate_callback": "rest_validate_request_arg" }, "after": { "description": "Limit response to resources published after a given ISO8601 compliant date.", "type": "string", "format": "date-time", "validate_callback": "rest_validate_request_arg" }, "author": { "description": "Limit result set to posts assigned to specific authors.", "type": "array", "default": [], "sanitize_callback": "wp_parse_id_list", "validate_callback": "rest_validate_request_arg" }, "author_exclude": { "description": "Ensure result set excludes posts assigned to specific authors.", "type": "array", "default": [], "sanitize_callback": "wp_parse_id_list", "validate_callback": "rest_validate_request_arg" }, "before": { "description": "Limit response to resources published before a given ISO8601 compliant date.", "type": "string", "format": "date-time", "validate_callback": "rest_validate_request_arg" }, "exclude": { "description": "Ensure result set excludes specific ids.", "type": "array", "default": [], "sanitize_callback": "wp_parse_id_list" }, "include": { "description": "Limit result set to specific ids.", "type": "array", "default": [], "sanitize_callback": "wp_parse_id_list" }, "offset": { "description": "Offset the result set by a specific number of items.", "type": "integer", "sanitize_callback": "absint", "validate_callback": "rest_validate_request_arg" }, "order": { "description": "Order sort attribute ascending or descending.", "type": "string", "default": "desc", "enum": [ "asc", "desc" ], "validate_callback": "rest_validate_request_arg" }, "orderby": { "description": "Sort collection by object attribute.", "type": "string", "default": "date", "enum": [ "date", "relevance", "id", "include", "title", "slug" ], "validate_callback": "rest_validate_request_arg" }, "slug": { "description": "Limit result set to posts with a specific slug.", "type": "string", "validate_callback": "rest_validate_request_arg" }, "status": { "default": "publish", "description": "Limit result set to posts assigned a specific status; can be comma-delimited list of status types.", "enum": [ "publish", "future", "draft", "pending", "private", "trash", "auto-draft", "inherit", "any" ], "sanitize_callback": "sanitize_key", "type": "string", "validate_callback": [ {}, "validate_user_can_query_private_statuses" ] }, "filter": { "description": "Use WP Query arguments to modify the response; private query vars require appropriate authorization." }, "categories": { "description": "Limit result set to all items that have the specified term assigned in the categories taxonomy.", "type": "array", "sanitize_callback": "wp_parse_id_list", "default": [] }, "tags": { "description": "Limit result set to all items that have the specified term assigned in the tags taxonomy.", "type": "array", "sanitize_callback": "wp_parse_id_list", "default": [] } }, "callback": [ {}, "get_items" ], "permission_callback": [ {}, "get_items_permissions_check" ] } As you can see we have all of the information we have registered to our endpoint already there, ready to go! The request attributes are typically used at a lower level and are handled by the WP_REST_Server class, however there are cool things that can be done inside of endpoint callbacks, like restricting accepted parameters to match registered arguments. The WP REST API is designed for you so that you do not have to mess around with any internals, so some of these more advanced methods of interacting with WP_REST_Request are not going to be commonly practiced. The core of using the WP REST API is linked to registering routes and endpoints. Requests are the tool we use to tell the API which endpoint we want to hit. This is most commonly done over HTTP, however we can also use WP_REST_Requests internally. Internal Requests The key to making internal requests is using rest_do_request(). All you need to do is pass in a request object and you will be returned a response. Because the request is never served by the WP_REST_Server, the response data is never encoded into json, meaning we have our response object as a PHP object. This is pretty awesome and enables us to do a lot of interesting things. For one, we can create efficient batch endpoints. From a performance perspective, one of the hurdles is minimizing HTTP requests. We can create batch endpoints that will use rest_do_request() to serve all of our requests internally all in one HTTP request. Here is a very simplistic batch endpoint for read only data, so you can see rest_do_request() in action. // Register our mock batch endpoint. function prefix_register_batch_route() { register_rest_route( 'my-namespace/v1', '/batch', array( // Supported methods for this endpoint. WP_REST_Server::READABLE translates to GET. 'methods' => WP_REST_Server::READABLE, // Register the callback for the endpoint. 'callback' => 'prefix_do_batch_request', // Register args for the batch endpoint. 'args' => prefix_batch_request_parameters(), ) ); } add_action( 'rest_api_init', 'prefix_register_batch_route' ); /** * Our registered endpoint callback. Notice how we are passing in $request as an argument. * By default, the WP_REST_Server will pass in the matched request object to our callback. * * @param WP_REST_Request $request The current matched request object. */ function prefix_do_batch_request( $request ) { // Here we initialize the array that will hold our response data. $data = array(); $data = prefix_handle_batch_requests( $request['requests'] ); return $data; } /** * This handles the building of the response for the batch requests we make. * * @param array $requests An array of data to build WP_REST_Request objects from. * @return WP_REST_Response A collection of response data for batch endpoints. */ function prefix_handle_batch_requests( $requests ) { $data = array(); // Foreach request specified in the requests param run the endpoint. foreach ( $requests as $request_params ) { $response = prefix_handle_request( $request_params ); $key = $request_params['method'] . ' ' . $request_params['route']; $data[ $key ] = prefix_prepare_for_collection( $response ); } return rest_ensure_response( $data ); } /** * This handles the building of the response for the batch requests we make. * * @param array $request_params Data to build a WP_REST_Request object from. * @return WP_REST_Response Response data for the request. */ function prefix_handle_request( $request_params ) { $request = new WP_REST_Request( $request_params['method'], $request_params['route'] ); // Add specified request parameters into the request. if ( isset( $request_params['params'] ) ) { foreach ( $request_params['params'] as $param_name => $param_value ) { $request->set_param( $param_name, $param_value ); } } $response = rest_do_request( $request ); return $response; } /** * Prepare a response for inserting into a collection of responses. * * This is lifted from WP_REST_Controller class in the WP REST API v2 plugin. * * @param WP_REST_Response $response Response object. * @return array Response data, ready for insertion into collection data. */ function prefix_prepare_for_collection( $response ) { if ( ! ( $response instanceof WP_REST_Response ) ) { return $response; } $data = (array) $response->get_data(); $server = rest_get_server(); if ( method_exists( $server, 'get_compact_response_links' ) ) { $links = call_user_func( array( $server, 'get_compact_response_links' ), $response ); } else { $links = call_user_func( array( $server, 'get_response_links' ), $response ); } if ( ! empty( $links ) ) { $data['_links'] = $links; } return $data; } /** * Returns the JSON schema data for our registered parameters. * * @return array $params A PHP representation of JSON Schema data. */ function prefix_batch_request_parameters() { $params = array(); $params['requests'] = array( 'description' => esc_html__( 'An array of request objects arguments that can be built into WP_REST_Request instances.', 'my-text-domain' ), 'type' => 'array', 'required' => true, 'validate_callback' => 'prefix_validate_requests', 'items' => array( array( 'type' => 'object', 'properties' => array( 'method' => array( 'description' => esc_html__( 'HTTP Method of the desired request.', 'my-text-domain' ), 'type' => 'string', 'required' => true, 'enum' => array( 'GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', ), ), 'route' => array( 'description' => esc_html__( 'Desired route for the request.', 'my-text-domain' ), 'required' => true, 'type' => 'string', 'format' => 'uri', ), 'params' => array( 'description' => esc_html__( 'Key value pairs of desired request parameters.', 'my-text-domain' ), 'type' => 'object', ), ), ), ), ); return $params; } function prefix_validate_requests( $requests, $request, $param_key ) { // If requests isn't an array of requests then we don't process the batch. if ( ! is_array( $requests ) ) { return new WP_Error( 'rest_invald_param', esc_html__( 'The requests parameter must be an array of requests.' ), array( 'status' => 400 ) ); } foreach ( $requests as $request ) { // If the method or route is not set then we do not run the requests. if ( ! isset( $request['method'] ) || ! isset( $request['route'] ) ) { return new WP_Error( 'rest_invald_param', esc_html__( 'You must specify the method and route for each request.' ), array( 'status' => 400 ) ); } if ( isset( $request['params'] ) && ! is_array( $request['params'] ) ) { return new WP_Error( 'rest_invald_param', esc_html__( 'You must specify the params for each request as an array of named key value pairs.' ), array( 'status' => 400 ) ); } } // This is a black listing approach to data validation. return true; } That is quite a decent chunk of code that covers a number of topics, but everything centers around what happens in prefix_handle_request(). Here we are passing in an array that tells us a HTTP method, a route, and a set of parameters we want to turn into a request. We then build the request object for the method and route. If any parameters were specified we use the WP_REST_Request::set_param() method to add in the desired parameters. Once our WP_REST_Request is ready to go we use rest_do_request to internally match that endpoint and the response is returned to our batch endpoint response collection. Using a batch endpoint like this can net you huge performance gains, as you will only make one HTTP request to get a response for multiple endpoints. The implementation of this is not necessarily the best and serves as an example; not the only way to do this. Responses Overview Responses in the API are what holds all of the data we want. If we made a mistake in our request, our response’s data should also inform us that an error occurred. Responses in the WordPress REST API should return the data we requested or an error message. Responses in the API are handled by the WP_REST_Response class, one of the three infrastructural classes for the API. WP_REST_Response The WP_REST_Response extends WordPress’s WP_HTTP_Response class, allowing us access to response headers, response status code, and response data. // The following code will not do anything and just serves as a demonstration. $response = new WP_REST_Response( 'This is some data' ); // To get the response data we can use this method. It should equal 'This is some data'. $our_data = $response->get_data(); // To access the HTTP status code we can use this method. The most common status code is probably 200, which means OK! $our_status = $response->get_status(); // To access the HTTP response headers we can use this method. $our_headers = $response->get_headers(); The above is pretty straightforward and shows you how to get what you need out of a response. The WP_REST_Response takes things a bit further. You can access the matched route for the response to backtrack which endpoint the response came from with $response->get_matched_route(). $response->get_matched_handler() will return the options registered for the endpoint that produced our response. These could be useful for logging the API among other things. The response class also helps us with error handling. Error Handling If something went terribly wrong in our request, we can return WP_Error objects in our endpoint callbacks explaining what went wrong, like this: // Register our mock batch endpoint. function prefix_register_broken_route() { register_rest_route( 'my-namespace/v1', '/broken', array( // Supported methods for this endpoint. WP_REST_Server::READABLE translates to GET. 'methods' => WP_REST_Server::READABLE, // Register the callback for the endpoint. 'callback' => 'prefix_get_an_error', ) ); } add_action( 'rest_api_init', 'prefix_register_broken_route' ); /** * Our registered endpoint callback. Notice how we are passing in $request as an argument. * By default, the WP_REST_Server will pass in the matched request object to our callback. * * @param WP_REST_Request $request The current matched request object. */ function prefix_get_an_error( $request ) { return new WP_Error( 'oops', esc_html__( 'This endpoint is currently broken, try another endpoint, I promise the API is cool! EEEK!!!!', 'my-textdomain' ), array( 'status' => 400 ) ); } That is kind of a silly example but it touches on some key things. The most important thing to understand is that the WordPress REST API will automatically handle changing the WP_Error object into an HTTP Response containing your data. When you set the status code in the WP_Error object your HTTP response status code will take on that value. This comes in really handy when you need to use different error codes like 404 for content that wasn’t found, or 403 for forbidden access. All we have to do is have our endpoint callbacks return a request and the WP_REST_Server class will handle a lot of really important things for us. There are other cool things the response class can help us with, like Linking. Linking What if we wanted to get a post and the first comment for that post? Would we write a separate endpoint to handle this use case? If we did that, we would need to start adding a lot of endpoints to handle various small use cases and our API index would get bloated really fast. Response Linking provides us a way to form relations between our resources that the API can understand. The API implements a standard known as HAL for resource linking. Let’s look at our post and comment example, it would be better to have routes for each resource. Let’s say we have post with ID = 1 and comment ID = 3. The comment is assigned to post 1, so realistically the two resources could live at the routes /my-namespace/v1/posts/1 and /my-namespace/v1/comments/3. We would add links to the responses to create the relationships between them. Let’s look at this from the comment perspective first. // Register our mock endpoints. function prefix_register_my_routes() { register_rest_route( 'my-namespace/v1', '/posts/(?P[\d]+)', array( // Supported methods for this endpoint. WP_REST_Server::READABLE translates to GET. 'methods' => WP_REST_Server::READABLE, // Register the callback for the endpoint. 'callback' => 'prefix_get_rest_post', ) ); register_rest_route( 'my-namespace/v1', '/comments', array( // Supported methods for this endpoint. WP_REST_Server::READABLE translates to GET. 'methods' => WP_REST_Server::READABLE, // Register the callback for the endpoint. 'callback' => 'prefix_get_rest_comments', // Register the post argument to limit results to a specific post parent. 'args' => array( 'post' => array( 'description' => esc_html__( 'The post ID that the comment is assigned to.', 'my-textdomain' ), 'type' => 'integer', 'required' => true, ), ), ) ); register_rest_route( 'my-namespace/v1', '/comments/(?P[\d]+)', array( // Supported methods for this endpoint. WP_REST_Server::READABLE translates to GET. 'methods' => WP_REST_Server::READABLE, // Register the callback for the endpoint. 'callback' => 'prefix_get_rest_comment', ) ); } add_action( 'rest_api_init', 'prefix_register_my_routes' ); // Grab a post. function prefix_get_rest_post( $request ) { $id = (int) $request['id']; $post = get_post( $id ); $response = rest_ensure_response( array( $post ) ); $response->add_links( prefix_prepare_post_links( $post ) ); return $response; } // Prepare post links. function prefix_prepare_post_links( $post ) { $links = array(); $replies_url = rest_url( 'my-namespace/v1/comments' ); $replies_url = add_query_arg( 'post', $post->ID, $replies_url ); $links['replies'] = array( 'href' => $replies_url, 'embeddable' => true, ); return $links; } // Grab a comments. function prefix_get_rest_comments( $request ) { if ( ! isset( $request['post'] ) ) { return new WP_Error( 'rest_bad_request', esc_html__( 'You must specify the post parameter for this request.', 'my-text-domain' ), array( 'status' => 400 ) ); } $data = array(); $comments = get_comments( array( 'post__in' => $request['post'] ) ); if ( empty( $comments ) ) { return array(); } foreach( $comments as $comment ) { $response = rest_ensure_response( $comment ); $response->add_links( prefix_prepare_comment_links( $comment ) ); $data[] = prefix_prepare_for_collection( $response ); } $response = rest_ensure_response( $data ); return $response; } // Grab a comment. function prefix_get_rest_comment( $request ) { $id = (int) $request['id']; $post = get_comment( $id ); $response = rest_ensure_response( $comment ); $response->add_links( prefix_prepare_comment_links( $comment ) ); return $response; } // Prepare comment links. function prefix_prepare_comment_links( $comment ) { $links = array(); if ( 0 !== (int) $comment->comment_post_ID ) { $post = get_post( $comment->comment_post_ID ); if ( ! empty( $post->ID ) ) { $links['up'] = array( 'href' => rest_url( 'my-namespace/v1/posts/' . $comment->comment_post_ID ), 'embeddable' => true, 'post_type' => $post->post_type, ); } } return $links; } /** * Prepare a response for inserting into a collection of responses. * * This is lifted from WP_REST_Controller class in the WP REST API v2 plugin. * * @param WP_REST_Response $response Response object. * @return array Response data, ready for insertion into collection data. */ function prefix_prepare_for_collection( $response ) { if ( ! ( $response instanceof WP_REST_Response ) ) { return $response; } $data = (array) $response->get_data(); $server = rest_get_server(); if ( method_exists( $server, 'get_compact_response_links' ) ) { $links = call_user_func( array( $server, 'get_compact_response_links' ), $response ); } else { $links = call_user_func( array( $server, 'get_response_links' ), $response ); } if ( ! empty( $links ) ) { $data['_links'] = $links; } return $data; } As you can see in the example above we are using links to create the relations between our resources. If the post has comments, our endpoint callback will add a link to the comments route specifying the `post` parameter to match our current post ID. So if you were to follow that route you would now get the comments that have that assigned post ID. If you search for comments then each comment will have a link point `up` to the post. `up` has special meaning in links using the HAL spec. If we follow an up link for a comment then we will be returned the post that is the comment parent. Linking is pretty awesome, but it gets better. The WordPress REST API also supports what is referred to as embedding. If you notice in both of the links we added, we specified that embeddable => true. This enables us to embed our linked data in our responses. So if we wanted to grab comment 3 and its assigned post we could make this request https://ourawesomesite.com/wp-json/my-namespace/v1/comments/3?_embed. The _embed parameter tells the API we want all of the embeddable resource links for our request to also be added to the API. Using embed is a performance gain as the multiple resources are all handled in one HTTP Request. Smart use of embedding and links make the WordPress REST API incredibly flexible and powerful for interacting with WordPress. REST API Overview The WordPress REST API brings many new features to WordPress. The REST API uses JSON (JavaScript Object Notation) as its data format.  JSON is an open standard data format that is becoming more widely used across the web as a whole, and software in general.  It is light-weight and human readable, and looks like Objects do in JavaScript; hence the name.  When you make a request to the API, the response will be returned in JSON. This enables developers to use WordPress in languages beyond PHP, which in turn allows WordPress to be used in new and exciting ways. Why use the WordPress REST API There are many use cases for the WordPress REST API.  One of the largest use cases is creating Single Page Applications on top of WordPress.  You could create an entirely new admin experience for WordPress, or you could create an entirely new front end experience for WordPress.  You would not even have to write the applications in PHP.  Any programming language that can make HTTP requests and interpret JSON could be used to write something on WordPress. The WordPress REST API can also serve as a strong replacement for the admin-ajax API in core.  By using the REST API, you can more easily structure the way you want to get data into and out of WordPress.  AJAX calls can be greatly simplified by using the REST API, enabling us to provide better user experiences in our work. The use cases extend beyond these and really our imagination is the only limit to what can be done.  The bottom line is, if you want an structured, extensible, and simple way to get data in and out of WordPress, you probably want to use the REST API.  The API, for all of its simplicity, can be quite complex at first and we will attempt to break it down into smaller components so that we can easily piece together the larger puzzle. Key Concepts To get started with using the WordPress REST API we will break down some of the key concepts and terms associated with the API: Routes/Endpoints Requests Responses Schema Controller Classes Each of these concepts play a crucial role in using and understanding the WordPress REST API.  Let’s briefly break them down so that we can later explore each in greater depth. Routes & Endpoints A route, in the context of the WordPress REST API, is a URI which can be mapped to different HTTP methods.  The mapping of an individual HTTP method to a route is known as an endpoint.  To clarify: If we make a GET request to http://oursite.com/wp-json/, we will get a JSON response showing us what routes are available, and within each route, what endpoints are available. /wp-json/ Is a route itself and when a GET request is made it matches to the endpoint that displays what is known as the index for the WordPress REST API. We will learn how to register our own routes and endpoints in the following sections. Requests In the WordPress REST API infrastructure one of the primary classes is WP_REST_Request. The request class is used to store and retrieve information for the current request, requests can also be made internally within PHP to avoid using HTTP. WP_REST_Request objects are automatically generated for you whenever you make an HTTP request to a registered route. The data specified in the request will have an impact on what response you get back out of the API. There are a lot of neat things that can be done using the request class. The request section will go into greater detail. Responses Responses are the data you get back from the API. The WP_REST_Response provides a way to interact with the response data returned by endpoints. Responses can return the desired data, and they can also be used to return errors. Schema When we have responses and requests of different kinds of data, we need to be able to tell what type of data we are interacting with. Schema provides us a way to structure our data. Schema also provides security benefits for the API as it enables us to validate requests being made to the API. Schema is a large topic and we will get into that in the schema section. Controller Classes As you can see the WordPress REST API has a lot of moving parts that all need to work together. Controller classes enable us to bring all of these elements together in a single place. With a controller class we will be able to manage the registering of routes & endpoints, handle requests, utilize schema, and generate responses. Next Steps Let’s dive into how to register routes and endpoints for the REST API. Routes & Endpoints The REST API provides a way to match URIs to various resources in our WordPress installation. By default, if you have pretty permalinks enabled, the WordPress REST API “lives” at /wp-json/. At our WordPress site https://ourawesomesite.com, we can access the REST API’s index by making a GET request to https://ourawesomesite.com/wp-json/. The index provides information regarding what routes are available for that particular WordPress install, along with what HTTP methods are supported and what endpoints are registered. If we wanted to create an endpoint that would return the phrase “Hello World, this is the WordPress REST API”, we would first need to register the route for that endpoint. To register routes you should use the register_rest_route() function. It needs to be called on the rest_api_init action hook. register_rest_route() handles all of the mapping for routes to endpoints. Let’s try to create a “Hello World, this is the WordPress REST API” route. /** * This is our callback function that embeds our phrase in a WP_REST_Response */ function prefix_get_endpoint_phrase() { // rest_ensure_response() wraps the data we want to return into a WP_REST_Response, and ensures it will be properly returned. return rest_ensure_response( 'Hello World, this is the WordPress REST API' ); } /** * This function is where we register our routes for our example endpoint. */ function prefix_register_example_routes() { // register_rest_route() handles more arguments but we are going to stick to the basics for now. register_rest_route( 'hello-world/v1', '/phrase', array( // By using this constant we ensure that when the WP_REST_Server changes our readable endpoints will work as intended. 'methods' => WP_REST_Server::READABLE, // Here we register our callback. The callback is fired when this endpoint is matched by the WP_REST_Server class. 'callback' => 'prefix_get_endpoint_phrase', ) ); } add_action( 'rest_api_init', 'prefix_register_example_routes' ); The first argument passed into register_rest_route() is the namespace, which provides us a way to group our routes. The second argument passed in is the resource path, or resource base. For our example, the resource we are retrieving is the “Hello World, this is the WordPress REST API” phrase. The third argument is an array of options. We specify what methods the endpoint can use and what callback should happen when the endpoint is matched (more things can be done but these are the fundamentals). The third argument also allows us to provide a permissions callback, which can restrict access for the endpoint to only certain users. The third argument also offers a way to register arguments for the endpoint so that requests can modify the response of our endpoint. We will get into those concepts in the endpoints section of this guide. When we go to https://ourawesomesite.com/wp-json/hello-world/v1/phrase we can now see our REST API greeting us kindly. Let’s take a look at routes a bit more in depth. Routes Routes in the REST API are represented by URIs. The route itself is what is tacked onto the end of https://ourawesomesite.com/wp-json. The index route for the API is '/' which is why https://ourawesomesite.com/wp-json/ returns all of the available information for the API. All routes should be built onto this route, the wp-json portion can be changed, but in general, it is advised to keep it the same. We want to make sure that our routes are unique. For instance we could have a route for books like this: /books. Our books route would now live at https://ourawesomesite.com/wp-json/books. However, this is not a good practice as we would end up polluting potential routes for the API. What if another plugin we wanted to register a books route as well? We would be in big trouble in that case, as the two routes would conflict with each other and only one could be used. The fourth parameter to register_rest_field() is a boolean for whether the route should override an existing route. The override parameter does not really solve our problem either, as both routes could override or we would want to use both routes for different things. This is where using namespaces for our routes comes in. Namespaces It is extremely important to add namespaces to your routes. The “core” endpoints, which are awaiting to be merged into WordPress core, use the /wp/v2 namespace. [info]DO NOT PLACE ANYTHING INTO THE /wp NAMESPACE UNLESS YOU ARE MAKING ENDPOINTS WITH THE INTENTION OF MERGING THEM INTO CORE.[/info] There are some key things to take notice of in the core endpoint namespace. The first part of the namespace is /wp, which represents the vendor name; WordPress. For our plugins we will want to come up with unique names for what we call the vendor portion of the namespace. In the example above we used hello-world. Following the vendor portion is the version portion of the namespace. The “core” endpoints utilize v2 to represent version 2 of the WordPress REST API. If you are writing a plugin, you can maintain backwards compatibility of your REST API endpoints by simply creating new endpoints and bumping up the version number you provide. This way both the original v1 and v2 endpoints can be accessed. The part of the route that follows the namespace is the resource path. Resource Paths The resource path should signify what resource the endpoint is associated with. In the example we used above, we used the word phrase to signify that the resource we are interacting with is a phrase. To avoid any collisions, each resource path we register should also be unique within a namespace. Resource paths should be used to define different resource routes within a given namespace. Let’s say we have a plugin that handles some basic eCommerce functionality. We will have two main resource types orders, and products. Orders are a request for product(s) but they are not the product themselves. The same concept applies to products. Although these resources are related they are not the same thing and each should live in a separate resource paths. Our routes will end up looking something like this for our eCommerce plugin: /my-shop/v1/orders and /my-shop/v1/products. Using routes like this, we would want each to return a collection of orders or products. What if we wanted to grab a specific product by ID, we would need to use path variables in our routes. Path Variables Path variables enable us to add dynamic routes. To expand on our eCommerce routes, we could register a route to grab individual products. /** * This is our callback function to return our products. * * @param WP_REST_Request $request This function accepts a rest request to process data. */ function prefix_get_products( $request ) { // In practice this function would fetch the desired data. Here we are just making stuff up. $products = array( '1' => 'I am product 1', '2' => 'I am product 2', '3' => 'I am product 3', ); return rest_ensure_response( $products ); } /** * This is our callback function to return a single product. * * @param WP_REST_Request $request This function accepts a rest request to process data. */ function prefix_get_product( $request ) { // In practice this function would fetch the desired data. Here we are just making stuff up. $products = array( '1' => 'I am product 1', '2' => 'I am product 2', '3' => 'I am product 3', ); // Here we are grabbing the 'id' path variable from the $request object. WP_REST_Request implements ArrayAccess, which allows us to grab properties as though it is an array. $id = (string) $request['id']; if ( isset( $products[ $id ] ) ) { // Grab the product. $product = $products[ $id ]; // Return the product as a response. return rest_ensure_response( $product ); } else { // Return a WP_Error because the request product was not found. In this case we return a 404 because the main resource was not found. return new WP_Error( 'rest_product_invalid', esc_html__( 'The product does not exist.', 'my-text-domain' ), array( 'status' => 404 ) ); } // If the code somehow executes to here something bad happened return a 500. return new WP_Error( 'rest_api_sad', esc_html__( 'Something went horribly wrong.', 'my-text-domain' ), array( 'status' => 500 ) ); } /** * This function is where we register our routes for our example endpoint. */ function prefix_register_product_routes() { // Here we are registering our route for a collection of products. register_rest_route( 'my-shop/v1', '/products', array( // By using this constant we ensure that when the WP_REST_Server changes our readable endpoints will work as intended. 'methods' => WP_REST_Server::READABLE, // Here we register our callback. The callback is fired when this endpoint is matched by the WP_REST_Server class. 'callback' => 'prefix_get_products', ) ); // Here we are registering our route for single products. The (?P[\d]+) is our path variable for the ID, which, in this example, can only be some form of positive number. register_rest_route( 'my-shop/v1', '/products/(?P[\d]+)', array( // By using this constant we ensure that when the WP_REST_Server changes our readable endpoints will work as intended. 'methods' => WP_REST_Server::READABLE, // Here we register our callback. The callback is fired when this endpoint is matched by the WP_REST_Server class. 'callback' => 'prefix_get_product', ) ); } add_action( 'rest_api_init', 'prefix_register_product_routes' ); The above example covers a lot. The important part to note is that in the second route we register, we add on a path variable /(?P[\d]+) to our resource path /products. The path variable is a regular expression. In this case it uses [\d]+ to signify that should be any numerical character at least once. If you are using numeric IDs for your resources, then this is a great example of how to use a path variable. When using path variables, we now have to be careful around what can be matched as it is user input. The regex luckily will filter out anything that is not numerical. However, what if the product for the requested ID doesn’t exist. We need to do error handling. You can see the basic way we are handling errors in the code example above. When you return a WP_Error in your endpoint callbacks the API server will automatically handle serving the error to the client. Although this section is about routes, we have covered quite a bit about endpoints. Endpoints and routes are interrelated, but they definitely have distinctions. Endpoints Endpoints are the destination that a route needs to map to. For any given route, you could have a number of different endpoints registered to it. We will expand on our fictitious eCommerce plugin, to better show the distinction between routes and endpoints. We are going to create two endpoints that exist at the /wp-json/my-shop/v1/products/ route. One endpoint uses the HTTP verb GET to get products, and the other endpoint uses the HTTP verb POST to create a new product. /** * This is our callback function to return our products. * * @param WP_REST_Request $request This function accepts a rest request to process data. */ function prefix_get_products( $request ) { // In practice this function would fetch the desired data. Here we are just making stuff up. $products = array( '1' => 'I am product 1', '2' => 'I am product 2', '3' => 'I am product 3', ); return rest_ensure_response( $products ); } /** * This is our callback function to return a single product. * * @param WP_REST_Request $request This function accepts a rest request to process data. */ function prefix_create_product( $request ) { // In practice this function would create a product. Here we are just making stuff up. return rest_ensure_response( 'Product has been created' ); } /** * This function is where we register our routes for our example endpoint. */ function prefix_register_product_routes() { // Here we are registering our route for a collection of products and creation of products. register_rest_route( 'my-shop/v1', '/products', array( array( // By using this constant we ensure that when the WP_REST_Server changes, our readable endpoints will work as intended. 'methods' => WP_REST_Server::READABLE, // Here we register our callback. The callback is fired when this endpoint is matched by the WP_REST_Server class. 'callback' => 'prefix_get_products', ), array( // By using this constant we ensure that when the WP_REST_Server changes, our create endpoints will work as intended. 'methods' => WP_REST_Server::CREATABLE, // Here we register our callback. The callback is fired when this endpoint is matched by the WP_REST_Server class. 'callback' => 'prefix_create_product', ), ) ); } add_action( 'rest_api_init', 'prefix_register_product_routes' ); Depending on what HTTP Method we use for the route /wp-json/my-shop/v1/products, we are matched to a different endpoint and a different callback is fired. When we use POST we trigger the prefix_create_product() callback, and when we use GET we trigger the prefix_get_products() callback. There are a number of different HTTP methods and the REST API can make use of any of them. HTTP Methods HTTP methods are sometimes referred to as HTTP verbs. They are simply just different ways to communicate via HTTP. The main ones used by the WordPress REST API are: GET should be used for retrieving data from the API. POST should be used for creating new resources (i.e users, posts, taxonomies). PUT should be used for updating resources. DELETE should be used for deleting resources. OPTIONS should be used to provide context about our resources. It is important to note that these methods are not supported by every client, as they were introduced in HTTP 1.1. Luckily, the API provides a workaround for these unfortunate cases. If you want to delete a resource but can’t send a DELETE request, then you can use the _method parameter or the X-HTTP-Method-Override header in your request. How this works is you will send a POST request to https://ourawesomesite.com/wp-json/my-shop/v1/products/1?_method=DELETE. Now you will have deleted product number 1, even though your client could not send the proper HTTP method in the request, or maybe there was a firewall in place that blocks out DELETE requests. The HTTP method, in combination with the route and callbacks, are what make up the core of an endpoint. Callbacks There are currently only two types of callbacks for endpoints supported by the REST API; callback and permissions_callback. The main callback should handle the interaction with the resource. The permissions callback should handle what users have access to the endpoint. You can add additional callbacks by adding additional information when registering an endpoint. You can then hook into rest_pre_dispatch, rest_dispatch_request, or rest_post_dispatch hooks to fire your new custom callbacks. Endpoint Callback The main callback for a delete endpoint should only delete the resource and return a copy of it in the response. The main callback for a creation endpoint should only create the resource and return a response matching the newly created data. An update callback should only modify resources that actually exist. A reading callback should only retrieve data that already exists. It is important to take into account the concept of idempotence. Idempotence, in the context of a REST API, means that if you make the same request to an endpoint the server will process the request the same way. Imagine if our read endpoint was not idempotent. Whenever we made a request to it the state of our server would be modified by the request, even though we were only trying to get data. This could be catastrophic. Any time someone fetched data from your server something would change internally. It is important to make sure that read, update, and delete endpoints do not have nasty side effects and just stick to what they are intended to do. In a REST API, the concept of idempotence is tied to HTTP methods instead of endpoint callbacks. Any callback using GET, HEAD, TRACE, OPTIONS, PUT, or DELETE, should not produce any side effects. POST requests are not idempotent, and are typically used for creating resources. If you created an idempotent creation method then you would only ever create one resource because when you make the same request there would be no more side effects to the server. For creating, if you make the same request over and over the server should generate new resources each time. To restrict usage of endpoints we need to register a permissions callback. Permissions Callback Permissions callbacks are extremely important for security with the WordPress REST API. If you have any private data that should not be displayed publicly, then you need to have permissions callbacks registered for your endpoints. Below is an example of how to register permissions callbacks. /** * This is our callback function that embeds our resource in a WP_REST_Response */ function prefix_get_private_data() { // rest_ensure_response() wraps the data we want to return into a WP_REST_Response, and ensures it will be properly returned. return rest_ensure_response( 'This is private data.' ); } /** * This is our callback function that embeds our resource in a WP_REST_Response */ function prefix_get_private_data_permissions_check() { // Restrict endpoint to only users who have the edit_posts capability. if ( ! current_user_can( 'edit_posts' ) ) { return new WP_Error( 'rest_forbidden', esc_html__( 'OMG you can not view private data.', 'my-text-domain' ), array( 'status' => 401 ) ); } // This is a black-listing approach. You could alternatively do this via white-listing, by returning false here and changing the permissions check. return true; } /** * This function is where we register our routes for our example endpoint. */ function prefix_register_example_routes() { // register_rest_route() handles more arguments but we are going to stick to the basics for now. register_rest_route( 'my-plugin/v1', '/private-data', array( // By using this constant we ensure that when the WP_REST_Server changes our readable endpoints will work as intended. 'methods' => WP_REST_Server::READABLE, // Here we register our callback. The callback is fired when this endpoint is matched by the WP_REST_Server class. 'callback' => 'prefix_get_private_data', // Here we register our permissions callback. The callback is fired before the main callback to check if the current user can access the endpoint. 'permissions_callback' => 'prefix_get_private_data_permissions_check', ) ); } add_action( 'rest_api_init', 'prefix_register_example_routes' ); If you try out this endpoint without any Authentication enabled then you will also be returned the error response, preventing you from seeing the data. Authentication is a huge topic and eventually a portion of this chapter will be created to show you how to create your own authentication processes. Arguments When making requests to an endpoint you might need to specify extra parameters to change the response. These extra parameters can be added while registering endpoints. Let’s look at an example of how to use arguments with an endpoint. /** * This is our callback function that embeds our resource in a WP_REST_Response */ function prefix_get_colors( $request ) { // In practice this function would fetch the desired data. Here we are just making stuff up. $colors = array( 'blue', 'blue', 'red', 'red', 'green', 'green', ); if ( isset( $request['filter'] ) ) { $filtered_colors = array(); foreach ( $colors as $color ) { if ( $request['filter'] === $color ) { $filtered_colors[] = $color; } } return rest_ensure_response( $filtered_colors ); } return rest_ensure_response( $colors ); } /** * We can use this function to contain our arguments for the example product endpoint. */ function prefix_get_color_arguments() { $args = array(); // Here we are registering the schema for the filter argument. $args['filter'] = array( // description should be a human readable description of the argument. 'description' => esc_html__( 'The filter parameter is used to filter the collection of colors', 'my-text-domain' ), // type specifies the type of data that the argument should be. 'type' => 'string', // enum specified what values filter can take on. 'enum' => array( 'red', 'green', 'blue' ), ); return $args; } /** * This function is where we register our routes for our example endpoint. */ function prefix_register_example_routes() { // register_rest_route() handles more arguments but we are going to stick to the basics for now. register_rest_route( 'my-colors/v1', '/colors', array( // By using this constant we ensure that when the WP_REST_Server changes our readable endpoints will work as intended. 'methods' => WP_REST_Server::READABLE, // Here we register our callback. The callback is fired when this endpoint is matched by the WP_REST_Server class. 'callback' => 'prefix_get_colors', // Here we register our permissions callback. The callback is fired before the main callback to check if the current user can access the endpoint. 'args' => prefix_get_color_arguments(), ) ); } add_action( 'rest_api_init', 'prefix_register_example_routes' ); We have now specified a filter argument for this example. We can specify the argument as a query parameter when we request the endpoint. If we make a GET request to https://ourawesomesitem.com/my-colors/v1/colors?filter=blue, we will be returned only the blue colors in our collection. You could also pass these as body parameters in the request body, instead of in the query string. To understand the distinction between query parameters and body parameters you should read about the HTTP spec. Query parameters live in the query string tacked onto the URL and body parameters are directly embedded in the body of an HTTP request. We have created an argument for our endpoint, but how do we verify that the argument is a string and tell whether it matches the value red, green, or blue. To do this we need to specify a validation callback for our argument. Validation Validation and sanitization are extremely important for security in the API. The validate callback (in WP 4.6+), fires before the sanitize callback. You should use the validate_callback for your arguments to verify whether the input you are receiving is valid. The sanitize_callback should be used to transform the argument input or clean out unwanted parts out of the argument, before the argument is processed by the main callback. In the example above, we need to verify that the filter parameter is a string, and it matches the value red, green, or blue. Let’s look at what the code looks like after adding in a validate_callback. /** * This is our callback function that embeds our resource in a WP_REST_Response */ function prefix_get_colors( $request ) { // In practice this function would fetch more practical data. Here we are just making stuff up. $colors = array( 'blue', 'blue', 'red', 'red', 'green', 'green', ); if ( isset( $request['filter'] ) ) { $filtered_colors = array(); foreach ( $colors as $color ) { if ( $request['filter'] === $color ) { $filtered_colors[] = $color; } } return rest_ensure_response( $filtered_colors ); } return rest_ensure_response( $colors ); } /** * Validate a request argument based on details registered to the route. * * @param mixed $value Value of the 'filter' argument. * @param WP_REST_Request $request The current request object. * @param string $param Key of the parameter. In this case it is 'filter'. * @return WP_Error|boolean */ function prefix_filter_arg_validate_callback( $value, $request, $param ) { // If the 'filter' argument is not a string return an error. if ( ! is_string( $value ) ) { return new WP_Error( 'rest_invalid_param', esc_html__( 'The filter argument must be a string.', 'my-text-domain' ), array( 'status' => 400 ) ); } // Get the registered attributes for this endpoint request. $attributes = $request->get_attributes(); // Grab the filter param schema. $args = $attributes['args'][ $param ]; // If the filter param is not a value in our enum then we should return an error as well. if ( ! in_array( $value, $args['enum'], true ) ) { return new WP_Error( 'rest_invalid_param', sprintf( __( '%s is not one of %s' ), $param, implode( ', ', $args['enum'] ) ), array( 'status' => 400 ) ); } } /** * We can use this function to contain our arguments for the example product endpoint. */ function prefix_get_color_arguments() { $args = array(); // Here we are registering the schema for the filter argument. $args['filter'] = array( // description should be a human readable description of the argument. 'description' => esc_html__( 'The filter parameter is used to filter the collection of colors', 'my-text-domain' ), // type specifies the type of data that the argument should be. 'type' => 'string', // enum specified what values filter can take on. 'enum' => array( 'red', 'green', 'blue' ), // Here we register the validation callback for the filter argument. 'validate_callback' => 'prefix_filter_arg_validate_callback', ); return $args; } /** * This function is where we register our routes for our example endpoint. */ function prefix_register_example_routes() { // register_rest_route() handles more arguments but we are going to stick to the basics for now. register_rest_route( 'my-colors/v1', '/colors', array( // By using this constant we ensure that when the WP_REST_Server changes our readable endpoints will work as intended. 'methods' => WP_REST_Server::READABLE, // Here we register our callback. The callback is fired when this endpoint is matched by the WP_REST_Server class. 'callback' => 'prefix_get_colors', // Here we register our permissions callback. The callback is fired before the main callback to check if the current user can access the endpoint. 'args' => prefix_get_color_arguments(), ) ); } add_action( 'rest_api_init', 'prefix_register_example_routes' ); Sanitizing In the above example, we do not need to use a sanitize_callback, because we are restricting input to only values in our enum. If we did not have strict validation and accepted any string as a parameter, we would definitely need to register a sanitize_callback. What if we wanted to update a content field and the user entered something like alert('ZOMG Hacking you');. The field value could potentially be a executable script. To strip out unwanted data or to transform data into a desired format we need to register a sanitize_callback for our arguments. Here is an example of how to use WordPress’s sanitize_text_field() for a sanitize callback: /** * This is our callback function that embeds our resource in a WP_REST_Response. * * The parameter is already sanitized by this point so we can use it without any worries. */ function prefix_get_item( $request ) { if ( isset( $request['data'] ) ) { return rest_ensure_response( $request['data'] ); } return new WP_Error( 'rest_invalid', esc_html__( 'The data parameter is required.', 'my-text-domain' ), array( 'status' => 400 ) ); } /** * Validate a request argument based on details registered to the route. * * @param mixed $value Value of the 'filter' argument. * @param WP_REST_Request $request The current request object. * @param string $param Key of the parameter. In this case it is 'filter'. * @return WP_Error|boolean */ function prefix_data_arg_validate_callback( $value, $request, $param ) { // If the 'data' argument is not a string return an error. if ( ! is_string( $value ) ) { return new WP_Error( 'rest_invalid_param', esc_html__( 'The filter argument must be a string.', 'my-text-domain' ), array( 'status' => 400 ) ); } } /** * Sanitize a request argument based on details registered to the route. * * @param mixed $value Value of the 'filter' argument. * @param WP_REST_Request $request The current request object. * @param string $param Key of the parameter. In this case it is 'filter'. * @return WP_Error|boolean */ function prefix_data_arg_sanitize_callback( $value, $request, $param ) { // It is as simple as returning the sanitized value. return sanitize_text_field( $value ); } /** * We can use this function to contain our arguments for the example product endpoint. */ function prefix_get_data_arguments() { $args = array(); // Here we are registering the schema for the filter argument. $args['data'] = array( // description should be a human readable description of the argument. 'description' => esc_html__( 'The data parameter is used to be sanitized and returned in the response.', 'my-text-domain' ), // type specifies the type of data that the argument should be. 'type' => 'string', // Set the argument to be required for the endpoint. 'required' => true, // We are registering a basic validation callback for the data argument. 'validate_callback' => 'prefix_data_arg_validate_callback', // Here we register the validation callback for the filter argument. 'sanitize_callback' => 'prefix_data_arg_sanitize_callback', ); return $args; } /** * This function is where we register our routes for our example endpoint. */ function prefix_register_example_routes() { // register_rest_route() handles more arguments but we are going to stick to the basics for now. register_rest_route( 'my-plugin/v1', '/sanitized-data', array( // By using this constant we ensure that when the WP_REST_Server changes our readable endpoints will work as intended. 'methods' => WP_REST_Server::READABLE, // Here we register our callback. The callback is fired when this endpoint is matched by the WP_REST_Server class. 'callback' => 'prefix_get_item', // Here we register our permissions callback. The callback is fired before the main callback to check if the current user can access the endpoint. 'args' => prefix_get_data_arguments(), ) ); } add_action( 'rest_api_init', 'prefix_register_example_routes' ); Summary We have covered the basics of registering endpoints for the WordPress REST API. Routes are the URIs that our endpoints live at. Endpoints are a collection of callbacks, methods, args, and other options. Each endpoint is mapped to a route when using register_rest_route(). An endpoint by default can support various HTTP methods, a main callback, a permissions callback, and registered arguments. We can register endpoints to cover any of our use cases for interacting with WordPress. The endpoints serve as the core interaction point with the REST API, but there are many other topics to explore and understand, to fully utilize this powerful API. Schema Overview Schema is data that tells us how are other data should be structured. Most databases implement some form of schema which enables us to reason about our data in a more structured manner. The WordPress REST API utilizes JSON Schema to handle the structuring of its data. You can implement endpoints without using schema, but you will be missing out on a lot of things. It is up to you to decide what suits you best. JSON Schema First, let’s talk about JSON a bit. JSON is a human readable data format that resembles JavaScript objects. JSON stands for JavaScript Object Notation. JSON is growing wildly in popularity and seems to be taking the world of data structure by storm. The WordPress REST API uses a special specification for JSON known as JSON schema. To learn more about JSON Schema please check out the JSON Schema website and this easier to understand introduction to JSON Schema. Schema affords us many benefits: improved testing, discoverability, and overall better structure. Let’s look at a JSON blob of data. { "shouldBeArray": 'LOL definitely not an array', "shouldBeInteger": ['lolz', 'you', 'need', 'schema'], "shouldBeString": 123456789 } A JSON parser will go through that data no problem and won’t complain about anything, because it is valid JSON. The clients and servers know nothing about the data and what to expect they just see the JSON. By implementing schema we can actually simplify our codebase. Schema will help structure our data better so our applications can more easily reason about our interactions with the WordPress REST API. The WordPress REST API does not force you to use schema, but it is encouraged. There are two ways in which schema data is incorporated into the API; schema for resources and schema for our registered arguments. Resource Schema The schema for a resource indicates what fields are present for a particular object. When we register our routes we can also specify the resource schema for the route. Let’s look at what a simple comment schema might look like in a PHP representation of JSON schema. // Register our routes. function prefix_register_my_comment_route() { register_rest_route( 'my-namespace/v1', '/comments', array( // Notice how we are registering multiple endpoints the 'schema' equates to an OPTIONS request. array( 'methods' => 'GET', 'callback' => 'prefix_get_comment_sample', ), // Register our schema callback. 'schema' => 'prefix_get_comment_schema', ) ); } add_action( 'rest_api_init', 'prefix_register_my_comment_route' ); /** * Grabs the five most recent comments and outputs them as a rest response. * * @param WP_REST_Request $request Current request. */ function prefix_get_comment_sample( $request ) { $args = array( 'post_per_page' => 5, ); $comments = get_comments( $args ); $data = array(); if ( empty( $comments ) ) { return rest_ensure_response( $data ); } foreach ( $comments as $comment ) { $response = prefix_rest_prepare_comment( $comment, $request ); $data[] = prefix_prepare_for_collection( $response ); } // Return all of our comment response data. return rest_ensure_response( $data ); } /** * Matches the comment data to the schema we want. * * @param WP_Comment $comment The comment object whose response is being prepared. */ function prefix_rest_prepare_comment( $comment, $request ) { $comment_data = array(); $schema = prefix_get_comment_schema( $request ); // We are also renaming the fields to more understandable names. if ( isset( $schema['properties']['id'] ) ) { $comment_data['id'] = (int) $comment->comment_id; } if ( isset( $schema['properties']['author'] ) ) { $comment_data['author'] = (int) $comment->user_id; } if ( isset( $schema['properties']['content'] ) ) { $comment_data['content'] = apply_filters( 'comment_text', $comment->comment_content, $comment ); } return rest_ensure_response( $comment_data ); } /** * Prepare a response for inserting into a collection of responses. * * This is copied from WP_REST_Controller class in the WP REST API v2 plugin. * * @param WP_REST_Response $response Response object. * @return array Response data, ready for insertion into collection data. */ function prefix_prepare_for_collection( $response ) { if ( ! ( $response instanceof WP_REST_Response ) ) { return $response; } $data = (array) $response->get_data(); $server = rest_get_server(); if ( method_exists( $server, 'get_compact_response_links' ) ) { $links = call_user_func( array( $server, 'get_compact_response_links' ), $response ); } else { $links = call_user_func( array( $server, 'get_response_links' ), $response ); } if ( ! empty( $links ) ) { $data['_links'] = $links; } return $data; } /** * Get our sample schema for comments. * * @param WP_REST_Request $request Current request. */ function prefix_get_comment_schema( $request ) { $schema = array( // This tells the spec of JSON Schema we are using which is draft 4. '$schema' => 'http://json-schema.org/draft-04/schema#', // The title property marks the identity of the resource. 'title' => 'comment', 'type' => 'object', // In JSON Schema you can specify object properties in the properties attribute. 'properties' => array( 'id' => array( 'description' => esc_html__( 'Unique identifier for the object.', 'my-textdomain' ), 'type' => 'integer', 'context' => array( 'view', 'edit', 'embed' ), 'readonly' => true, ), 'author' => array( 'description' => esc_html__( 'The id of the user object, if author was a user.', 'my-textdomain' ), 'type' => 'integer', ), 'content' => array( 'description' => esc_html__( 'The content for the object.', 'my-textdomain' ), 'type' => 'string', ), ), ); return $schema; } If you notice, each comment resource now matches up to our schema that we specified. We made this switch in prefix_rest_prepare_comment(). By creating schema for our resources, we can now view this schema by making OPTIONS requests. Why is this useful? If we wanted other languages, JavaScript for example, to interpret our data and validate the data from our endpoint, JavaScript would need to know how our data is structured. When we provide schema, we open the doors for other authors, and ourselves, to build on top of our endpoints in a consistent manner. Schema provides machine readable data, so potentially anything that can read JSON can understand what kind of data it is looking at. When we look at the API index by making a GET request to https://ourawesomesite.com/wp-json/, we are returned the schema of our API, enabling others to write client libraries to interpret our data. This process of reading schema data is known as discovery. When we have provided schema for a resource we make that resource discoverable via OPTIONS requests to that route. Exposing resource schema is only one part of our schema puzzle. We also want to use schema for our registered arguments. Argument Schema When we register request arguments for an endpoint, we can also use JSON Schema to provide us data about what the arguments should be. This enables us to write validation libraries that can be reused as our endpoints expand. Schema is more work upfront, but if you are going to write a production application that will grow, you should definitely consider using schema. Let’s look at an example of using argument schema and validation. // Register our routes. function prefix_register_my_arg_route() { register_rest_route( 'my-namespace/v1', '/schema-arg', array( // Here we register our endpoint. array( 'methods' => 'GET', 'callback' => 'prefix_get_item', 'args' => prefix_get_endpoint_args(), ), ) ); } // Hook registration into 'rest_api_init' hook. add_action( 'rest_api_init', 'prefix_register_my_arg_route' ); /** * Returns the request argument `my-arg` as a rest response. * * @param WP_REST_Request $request Current request. */ function prefix_get_item( $request ) { // If we didn't use required in the schema this would throw an error when my arg is not set. return rest_ensure_response( $request['my-arg'] ); } /** * Get the argument schema for this example endpoint. */ function prefix_get_endpoint_args() { $args = array(); // Here we add our PHP representation of JSON Schema. $args['my-arg'] = array( 'description' => esc_html__( 'This is the argument our endpoint returns.', 'my-textdomain' ), 'type' => 'string', 'validate_callback' => 'prefix_validate_my_arg', 'sanitize_callback' => 'prefix_sanitize_my_arg', 'required' => true, ); return $args; } /** * Our validation callback for `my-arg` parameter. * * @param mixed $value Value of the my-arg parameter. * @param WP_REST_Request $request Current request object. * @param string $param The name of the parameter in this case, 'my-arg'. */ function prefix_validate_my_arg( $value, $request, $param ) { $attributes = $request->get_attributes(); if ( isset( $attributes['args'][ $param ] ) ) { $argument = $attributes['args'][ $param ]; // Check to make sure our argument is a string. if ( 'string' === $argument['type'] && ! is_string( $value ) ) { return new WP_Error( 'rest_invalid_param', sprintf( esc_html__( '%1$s is not of type %2$s', 'my-textdomain' ), $param, 'string' ), array( 'status' => 400 ) ); } } else { // This code won't execute because we have specified this argument as required. // If we reused this validation callback and did not have required args then this would fire. return new WP_Error( 'rest_invalid_param', sprintf( esc_html__( '%s was not registered as a request argument.', 'my-textdomain' ), $param ), array( 'status' => 400 ) ); } // If we got this far then the data is valid. return true; } /** * Our santization callback for `my-arg` parameter. * * @param mixed $value Value of the my-arg parameter. * @param WP_REST_Request $request Current request object. * @param string $param The name of the parameter in this case, 'my-arg'. */ function prefix_sanitize_my_arg( $value, $request, $param ) { $attributes = $request->get_attributes(); if ( isset( $attributes['args'][ $param ] ) ) { $argument = $attributes['args'][ $param ]; // Check to make sure our argument is a string. if ( 'string' === $argument['type'] ) { return sanitize_text_field( $value ); } } else { // This code won't execute because we have specified this argument as required. // If we reused this validation callback and did not have required args then this would fire. return new WP_Error( 'rest_invalid_param', sprintf( esc_html__( '%s was not registered as a request argument.', 'my-textdomain' ), $param ), array( 'status' => 400 ) ); } // If we got this far then something went wrong don't use user input. return new WP_Error( 'rest_api_sad', esc_html__( 'Something went terribly wrong.', 'my-textdomain' ), array( 'status' => 500 ) ); } In the example above we have abstracted away from using the 'my-arg' name. We can use these validation and sanitizing functions for any other argument that should be a string we have specified schema for. As your codebase and endpoints grow, schema will help keep your code lightweight and maintainable. Without schema you can validate and sanitize, however it will be more difficult to keep track of which functions should be validating what. By adding schema to request arguments we can also expose our argument schema to clients, so validation libraries can be built client side which can help performance by preventing invalid requests from ever being sent to the API. [info]If you are uncomfortable with using schema, it is still possible to have validate/sanitize callbacks for each of your arguments, and in some cases it will make the most sense to do a custom validation.[/info] Overview Schema can seem silly at points and possibly like unnecessary work, but if you want maintainable, discoverable, and easily extensible endpoints, it is essential to use schema. Schema also helps to self document your endpoints both for humans and computers!