Creating Routes
We have already discussed what Routes and Endpoints are here, and in this section, we will look at how to create your own routes with their own endpoints.
Creating routes is based on just one function register_rest_route(). Essentially, all you need to do to create your route and endpoints is to understand how to use this function (see its description).
Below, we will examine a simple example of creating a route and then expand it to create a route based on a controller class.
Simple Example of Creating a Route
Let’s start by creating a simple PHP function.
## Gets posts by the specified author function my_awesome_func( WP_REST_Request $request ){ $posts = get_posts( array( 'author' => (int) $request['id'], ) ); if ( empty( $posts ) ) return new WP_Error( 'no_author_posts', 'No records found', [ 'status' => 404 ] ); return $posts; }
To make the result of this function available from the API, we need to register a route and attach this function to it. Then, when a request is made to this route, the user will see the response from this function.
The route is registered using the function register_rest_route() on the hook rest_api_init, to avoid performing any operations if the REST API is disabled.
You need to pass three parameters to the register_rest_route() function: namespace, route, and route options (in the options, we specify the name of our PHP function).
add_action( 'rest_api_init', function(){ register_rest_route( 'myplugin/v1', '/author-posts/(?P<id>\d+)', [ 'methods' => 'GET', 'callback' => 'my_awesome_func', ] ); } );
Why the namespace is called myplugin/v1
, read here.
The route /author-posts/(?P<id>\d+)
is specified as a regular expression because we want posts by the specified author to be returned when the URL /author-posts/{id}
is accessed, where {id} is replaced by the actual author's ID.
Now, by following the link http://example.com/wp-json/myplugin/v1/author-posts/1
on our site, we will see the JSON response returned by our function my_awesome_func().
A GET request to the URL http://example.com/wp-json/myplugin/v1/author-posts/1
is the endpoint we described when registering the route.
A route can have multiple endpoints, and different parameters can be specified for each endpoint:
- methods — HTTP methods to access the endpoint
- callback — callback function to respond to the request
- permission_callback — callback function to check access rights to the endpoint.
- args — request parameters that can be passed to the endpoint. Each parameter is described as an array element. For each parameter, you can specify your options, such as whether this parameter is required (required) or the default value of the parameter (default), and a validation function for the parameter value (validate_callback).
For more details on all parameters and arguments, see the description of the function register_rest_route().
To make it clearer, let’s expand our route and add another endpoint that can only be accessed by an authenticated user and only via a POST request, which can accept two parameters: arg_str and arg_int.
add_action( 'rest_api_init', function(){ register_rest_route( 'myplugin/v1', '/author-posts/(?P<id>\d+)', array( array( 'methods' => 'GET', 'callback' => 'my_awesome_func', ), array( 'methods' => 'POST', 'callback' => 'my_awesome_func_two', 'args' => array( 'arg_str' => array( 'type' => 'string', // the parameter value must be a string 'required' => true, // the parameter is required ), 'arg_int' => array( 'type' => 'integer', // the parameter value must be a number 'default' => 10, // default value = 10 ), ), 'permission_callback' => function( $request ){ // only authenticated users have access to the endpoint return is_user_logged_in(); }, // or in this case, it can be written more simply 'permission_callback' => 'is_user_logged_in', ) ) ); } );
In this example, for the second endpoint (POST), the function my_awesome_func_two is used, which should return a response if the user is authenticated and the required parameter arg_str is specified. It will also receive two request parameters. We do not yet have the function my_awesome_func_two(), so let’s create it; it will return the passed request parameters in response:
function my_awesome_func_two( WP_REST_Request $request ){ $response = array( 'arg_str' => $request->get_param('arg_str'), 'arg_int' => $request->get_param('arg_int') ); return $response; }
Checking the Created Route
Now let’s check how everything works by trying to send requests to our endpoints.
Sending a GET request:
GET http://example.com/wp-json/myplugin/v1/author-posts/1
We will receive a response:
[ { "ID": 72, "post_author": "1", "post_content": "Post content", "post_title": "Post title", ... "comment_count": "0", }, { ... }, { ... } ]
Sending a POST request:
POST http://example.com/wp-json/myplugin/v1/author-posts/1
We receive an error because we are not authenticated:
{ "code": "empty_username", "message": "<strong>ERROR</strong>: The username field is empty.", "data": null, "additional_errors": [ { "code": "empty_password", "message": "<strong>ERROR</strong>: The password field is empty.", "data": null } ] }
Authenticate, send the request again, and we receive an error because we did not specify the required parameter 'arg_str'.
{ "code": "rest_missing_callback_param", "message": "Missing parameter: arg_str", "data": { "status": 400, "params": [ "arg_str" ] } }
Let’s specify the parameters:
POST http://example.com/wp-json/myplugin/v1/author-posts/1?arg_str=foo&arg_int=bar
We receive an error again because the type of the parameter arg_int is specified as integer, but we provided a string:
{ "code": "rest_invalid_param", "message": "Invalid parameter: arg_int", "data": { "status": 400, "params": { "arg_int": "arg_int does not belong to the type integer." } } }
Let’s correct the value:
POST http://example.com/wp-json/myplugin/v1/author-posts/1?arg_str=foo&arg_int=123
We get the expected response:
{ "arg_str": "foo", "arg_int": 123 }
Explanations of Arguments When Creating a Route
See the description of the function register_rest_route().
Creating a Route Based on a Controller Class (OOP)
For more details on Controller Classes, read in the corresponding section.
When registering a new route and its endpoints, it is recommended to use a controller class because it makes the code more convenient, stable, and easily extensible.
The essence of creating a controller class lies in the convenience of code and its extensibility — to create one class that will work with one resource. However, it can have multiple routes, different parameters for the routes, a unified resource schema (which, by the way, can be described when creating the route), and much more.
That is, by creating a controller class, we also register new routes, but at the same time create logical, extensible code based on OOP.
What a controller class should be like has been thoughtfully designed by WP developers and described in the abstract class WP_REST_Controller.
The tasks of the controller include:
- Receiving input data.
- Generating output data.
In the WP REST API, our controllers will receive requests as an object WP_REST_Request and can generate any response; for convenience, they can return a response as an object WP_REST_Response (see rest_ensure_response()).
Example of Creating a Controller Class
GitHub// Запускаем наш контроллер и регистрируем маршруты add_action( 'rest_api_init', 'prefix_register_my_rest_routes' ); function prefix_register_my_rest_routes() { $controller = new My_REST_Posts_Controller(); $controller->register_routes(); } class My_REST_Posts_Controller extends WP_REST_Controller { function __construct(){ $this->namespace = 'my-namespace/v1'; $this->rest_base = 'posts'; } function register_routes(){ register_rest_route( $this->namespace, "/$this->rest_base", [ [ 'methods' => 'GET', 'callback' => [ $this, 'get_items' ], 'permission_callback' => [ $this, 'get_items_permissions_check' ], ], 'schema' => [ $this, 'get_item_schema' ], ] ); register_rest_route( $this->namespace, "/$this->rest_base/(?P<id>[\w]+)", [ [ 'methods' => 'GET', 'callback' => [ $this, 'get_item' ], 'permission_callback' => [ $this, 'get_item_permissions_check' ], ], 'schema' => [ $this, 'get_item_schema' ], ] ); } 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.' ), [ 'status' => $this->error_status_code() ] ); return true; } /** * Получает последние посты и отдает их в виде rest ответа. * * @param WP_REST_Request $request Текущий запрос. * * @return WP_Error|array */ function get_items( $request ){ $data = []; $posts = get_posts( [ 'post_per_page' => 5, ] ); if ( empty( $posts ) ) return $data; foreach( $posts as $post ){ $response = $this->prepare_item_for_response( $post, $request ); $data[] = $this->prepare_response_for_collection( $response ); } return $data; } ## Проверка права доступа. function get_item_permissions_check( $request ){ return $this->get_items_permissions_check( $request ); } /** * Получает отдельный ресурс. * * @param WP_REST_Request $request Текущий запрос. * * @return array */ function get_item( $request ){ $id = (int) $request['id']; $post = get_post( $id ); if( ! $post ) return array(); return $this->prepare_item_for_response( $post, $request ); } /** * Собирает данные ресурса в соответствии со схемой ресурса. * * @param WP_Post $post Объект ресурса, из которого будут взяты оригинальные данные. * @param WP_REST_Request $request Текущий запрос. * * @return array */ function prepare_item_for_response( $post, $request ){ $post_data = []; $schema = $this->get_item_schema(); // 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 $post_data; } /** * Подготавливает ответ отдельного ресурса для добавления его в коллекцию ресурсов. * * @param WP_REST_Response $response Response object. * * @return array|mixed Response data, ready for insertion into collection data. */ 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( [ $server, 'get_compact_response_links' ], $response ); } else { $links = call_user_func( [ $server, 'get_response_links' ], $response ); } if ( ! empty( $links ) ){ $data['_links'] = $links; } return $data; } ## Схема ресурса. function get_item_schema(){ $schema = [ // показывает какую версию схемы мы используем - это draft 4 '$schema' => 'http://json-schema.org/draft-04/schema#', // определяет ресурс который описывает схема 'title' => 'vehicle', 'type' => 'object', // в JSON схеме нужно указывать свойства в атрибуете 'properties'. 'properties' => [ 'id' => [ 'description' => 'Unique identifier for the object.', 'type' => 'integer', 'context' => [ 'view', 'edit', 'embed' ], 'readonly' => true, ], 'vin' => [ 'description' => 'VIN code of vehicle.', 'type' => 'string', ], // TODO добавить поля // [] ], ]; return $schema; } ## Устанавливает HTTP статус код для авторизации. function error_status_code(){ return is_user_logged_in() ? 403 : 401; } }
You can find more examples of Controller Classes in the WordPress core. Here are some of them: