Using the REST API (v2) javascript client on a private namespaced route

I am using the latest version of the REST API plugin in a project and I’ve added my routes to a separate namespace (as recommended in the documentation). The javascript client included with the API automatically creates models and collections based on the public routes in the wp/v2 namespace.

Does anyone know if it’s possible to extend the built-in client to use a custom namespace, and automatically parse the root endpoint to generate the models and collections for my private API?

1 Answer
1

To modify the rest url prefix you can filter rest_url_prefix. But that just changes the /wp-json/ prefix that every namespace uses.

Trying to modify wp/v2 means modifying the plugin and that namespace is hard-coded in several places like; WP_REST_Post_Statuses_Controller.

To add your own custom endpoints, register_rest_route is in core to do just that.

<?php
add_action( 'rest_api_init', function () {
    register_rest_route( 'your-namespace-here/v1', '/author/(?P<id>\d+)', array(
        'methods' => 'GET',
        'callback' => 'my_awesome_func',
    ) );
} );

You’d have to look more into the discovery process to see what automatically is built out for your API.

The controller pattern example for the WP REST API Plugin should give you a good idea of what can go into a WP REST Controller.

add_action( 'rest_api_init', function () {

    /*
     *      /wp-json/vendor/v1
     */

    $routes = new Slug_Custom_Route();
    $routes->register_routes();

});


class Slug_Custom_Route extends WP_REST_Controller {

    /**
     * Register the routes for the objects of the controller.
     */
    public function register_routes() {
        $version = '1';
        $namespace="vendor/v" . $version;
        $base="route";
        register_rest_route( $namespace, "https://wordpress.stackexchange.com/" . $base, array(
            array(
                'methods'         => WP_REST_Server::READABLE,
                'callback'        => array( $this, 'get_items' ),
                'permission_callback' => array( $this, 'get_items_permissions_check' ),
                'args'            => array(

                ),
            ),
            array(
                'methods'         => WP_REST_Server::CREATABLE,
                'callback'        => array( $this, 'create_item' ),
                'permission_callback' => array( $this, 'create_item_permissions_check' ),
                'args'            => $this->get_endpoint_args_for_item_schema( true ),
            ),
        ) );
        register_rest_route( $namespace, "https://wordpress.stackexchange.com/" . $base . '/(?P<id>[\d]+)', array(
            array(
                'methods'         => WP_REST_Server::READABLE,
                'callback'        => array( $this, 'get_item' ),
                'permission_callback' => array( $this, 'get_item_permissions_check' ),
                'args'            => array(
                    'context'          => array(
                        'default'      => 'view',
                    ),
                ),
            ),
            array(
                'methods'         => WP_REST_Server::EDITABLE,
                'callback'        => array( $this, 'update_item' ),
                'permission_callback' => array( $this, 'update_item_permissions_check' ),
                'args'            => $this->get_endpoint_args_for_item_schema( false ),
            ),
            array(
                'methods'  => WP_REST_Server::DELETABLE,
                'callback' => array( $this, 'delete_item' ),
                'permission_callback' => array( $this, 'delete_item_permissions_check' ),
                'args'     => array(
                    'force'    => array(
                        'default'      => false,
                    ),
                ),
            ),
        ) );
        register_rest_route( $namespace, "https://wordpress.stackexchange.com/" . $base . '/schema', array(
            'methods'         => WP_REST_Server::READABLE,
            'callback'        => array( $this, 'get_public_item_schema' ),
        ) );
    }

    /**
     * Get a collection of items
     *
     * @param WP_REST_Request $request Full data about the request.
     * @return WP_Error|WP_REST_Response
     */
    public function get_items( $request ) {
        $items = array(); //do a query, call another class, etc
        $data = array();
        foreach( $items as $item ) {
            $itemdata = $this->prepare_item_for_response( $item, $request );
            $data[] = $this->prepare_response_for_collection( $itemdata );
        }

        return new WP_REST_Response( $data, 200 );
    }

    /**
     * Get one item from the collection
     *
     * @param WP_REST_Request $request Full data about the request.
     * @return WP_Error|WP_REST_Response
     */
    public function get_item( $request ) {
        //get parameters from request
        $params = $request->get_params();
        $item = array();//do a query, call another class, etc
        $data = $this->prepare_item_for_response( $item, $request );

        //return a response or error based on some conditional
        if ( 1 == 1 ) {
            return new WP_REST_Response( $data, 200 );
        }else{
            return new WP_Error( 'code', __( 'message', 'text-domain' ) );
        }
    }

    /**
     * Create one item from the collection
     *
     * @param WP_REST_Request $request Full data about the request.
     * @return WP_Error|WP_REST_Request
     */
    public function create_item( $request ) {

        $item = $this->prepare_item_for_database( $request );

        if ( function_exists( 'slug_some_function_to_create_item')  ) {
            $data = slug_some_function_to_create_item( $item );
            if ( is_array( $data ) ) {
                return new WP_REST_Response( $data, 200 );
            }
        }

        return new WP_Error( 'cant-create', __( 'message', 'text-domain'), array( 'status' => 500 ) );


    }

    /**
     * Update one item from the collection
     *
     * @param WP_REST_Request $request Full data about the request.
     * @return WP_Error|WP_REST_Request
     */
    public function update_item( $request ) {
        $item = $this->prepare_item_for_database( $request );

        if ( function_exists( 'slug_some_function_to_update_item')  ) {
            $data = slug_some_function_to_update_item( $item );
            if ( is_array( $data ) ) {
                return new WP_REST_Response( $data, 200 );
            }
        }

        return new WP_Error( 'cant-update', __( 'message', 'text-domain'), array( 'status' => 500 ) );

    }

    /**
     * Delete one item from the collection
     *
     * @param WP_REST_Request $request Full data about the request.
     * @return WP_Error|WP_REST_Request
     */
    public function delete_item( $request ) {
        $item = $this->prepare_item_for_database( $request );

        if ( function_exists( 'slug_some_function_to_delete_item')  ) {
            $deleted = slug_some_function_to_delete_item( $item );
            if (  $deleted  ) {
                return new WP_REST_Response( true, 200 );
            }
        }

        return new WP_Error( 'cant-delete', __( 'message', 'text-domain'), array( 'status' => 500 ) );
    }

    /**
     * Check if a given request has access to get items
     *
     * @param WP_REST_Request $request Full data about the request.
     * @return WP_Error|bool
     */
    public function get_items_permissions_check( $request ) {
        //return true; <--use to make readable by all
        return current_user_can( 'edit_something' );
    }

    /**
     * Check if a given request has access to get a specific item
     *
     * @param WP_REST_Request $request Full data about the request.
     * @return WP_Error|bool
     */
    public function get_item_permissions_check( $request ) {
        return $this->get_items_permissions_check( $request );
    }

    /**
     * Check if a given request has access to create items
     *
     * @param WP_REST_Request $request Full data about the request.
     * @return WP_Error|bool
     */
    public function create_item_permissions_check( $request ) {
        return current_user_can( 'edit_something' );
    }

    /**
     * Check if a given request has access to update a specific item
     *
     * @param WP_REST_Request $request Full data about the request.
     * @return WP_Error|bool
     */
    public function update_item_permissions_check( $request ) {
        return $this->create_item_permissions_check( $request );
    }

    /**
     * Check if a given request has access to delete a specific item
     *
     * @param WP_REST_Request $request Full data about the request.
     * @return WP_Error|bool
     */
    public function delete_item_permissions_check( $request ) {
        return $this->create_item_permissions_check( $request );
    }

    /**
     * Prepare the item for create or update operation
     *
     * @param WP_REST_Request $request Request object
     * @return WP_Error|object $prepared_item
     */
    protected function prepare_item_for_database( $request ) {
        return array();
    }

    /**
     * Prepare the item for the REST response
     *
     * @param mixed $item WordPress representation of the item.
     * @param WP_REST_Request $request Request object.
     * @return mixed
     */
    public function prepare_item_for_response( $item, $request ) {
        return array();
    }

    /**
     * Get the query params for collections
     *
     * @return array
     */
    public function get_collection_params() {
        return array(
            'page'                   => array(
                'description'        => 'Current page of the collection.',
                'type'               => 'integer',
                'default'            => 1,
                'sanitize_callback'  => 'absint',
            ),
            'per_page'               => array(
                'description'        => 'Maximum number of items to be returned in result set.',
                'type'               => 'integer',
                'default'            => 10,
                'sanitize_callback'  => 'absint',
            ),
            'search'                 => array(
                'description'        => 'Limit results to those matching a string.',
                'type'               => 'string',
                'sanitize_callback'  => 'sanitize_text_field',
            ),
        );
    }
}

And when you go directly to the namespace’s endpoint /wp-json/vendor/v1 you should see:

{
    "namespace": "vendor\/v1",
    "routes": {
        "\/vendor\/v1": {
            "namespace": "vendor\/v1",
            "methods": ["GET"],
            "endpoints": [{
                "methods": ["GET"],
                "args": {
                    "namespace": {
                        "required": false,
                        "default": "vendor\/v1"
                    },
                    "context": {
                        "required": false,
                        "default": "view"
                    }
                }
            }],
            "_links": {
                "self": "http:\/\/example.com\/wp-json\/vendor\/v1"
            }
        },
        "\/vendor\/v1\/route": {
            "namespace": "vendor\/v1",
            "methods": ["GET", "POST", "GET", "POST"],
            "endpoints": [{
                "methods": ["GET"],
                "args": []
            }, {
                "methods": ["POST"],
                "args": []
            }, {
                "methods": ["GET"],
                "args": []
            }, {
                "methods": ["POST"],
                "args": []
            }],
            "_links": {
                "self": "http:\/\/example.com\/wp-json\/vendor\/v1\/route"
            }
        },
        "\/vendor\/v1\/route\/(?P<id>[\\d]+)": {
            "namespace": "vendor\/v1",
            "methods": ["GET", "POST", "PUT", "PATCH", "DELETE", "GET", "POST", "PUT", "PATCH", "DELETE"],
            "endpoints": [{
                "methods": ["GET"],
                "args": {
                    "context": {
                        "required": false,
                        "default": "view"
                    }
                }
            }, {
                "methods": ["POST", "PUT", "PATCH"],
                "args": []
            }, {
                "methods": ["DELETE"],
                "args": {
                    "force": {
                        "required": false,
                        "default": false
                    }
                }
            }, {
                "methods": ["GET"],
                "args": {
                    "context": {
                        "required": false,
                        "default": "view"
                    }
                }
            }, {
                "methods": ["POST", "PUT", "PATCH"],
                "args": []
            }, {
                "methods": ["DELETE"],
                "args": {
                    "force": {
                        "required": false,
                        "default": false
                    }
                }
            }]
        },
        "\/vendor\/v1\/route\/schema": {
            "namespace": "vendor\/v1",
            "methods": ["GET", "GET"],
            "endpoints": [{
                "methods": ["GET"],
                "args": []
            }, {
                "methods": ["GET"],
                "args": []
            }],
            "_links": {
                "self": "http:\/\/example.com\/wp-json\/vendor\/v1\/route\/schema"
            }
        }
    },
    "_links": {
        "up": [{
            "href": "http:\/\/example.com\/wp-json\/"
        }]
    }
}

The two main things that stick out would be:

'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE )

and

'args'     => array (
    'force' => array (
        'default' => false,
    ),
),

Another example of passing $args to register_rest_route which would show up at the namespace root:

register_rest_route( "{$root}/{$version}", '/products', array(
        array(
            'methods'         => \WP_REST_Server::READABLE,
            'callback'        => array( $cb_class, 'get_items' ),
            'args'            => array(
                'per_page' => array(
                    'default' => 10,
                    'sanitize_callback' => 'absint',
                ),
                'page' => array(
                    'default' => 1,
                    'sanitize_callback' => 'absint',
                ),
                'soon' => array(
                    'default' => 0,
                    'sanitize_callback' => 'absint',
                ),
                'slug' => array(
                    'default' => false,
                    'sanitize_callback' => 'sanitize_title',
                )

            ),

            'permission_callback' => array( $this, 'permissions_check' )
        ),
    )

);

Looking a bit further, get_data_for_route is used to collected what ends up in the discovery endpoints and it’s filterable with rest_endpoints_description or get_data_for_route. So in theory, if the default doesn’t give you what you want, you can use that filter to add your models and/or collections for your private API.


So putting it all together and overriding the automatic endpoints:

function my_awesome_func( WP_REST_Request $request) {
    return "awesome! " . $request->get_param( 'id' );
}

add_action( 'rest_api_init', function() {
    register_rest_route( 'my-thing/v1', '/awesome/(?P<id>\d+)', array (
        'methods'  => 'GET',
        'callback' => 'my_awesome_func',
    ) );
} );

add_filter( 'rest_endpoints_description', function( $data ) {

    if ( $data[ 'namespace' ] === 'my-thing/v1' ) {

        $data[ 'endpoints' ] = array (
            'foo'     => 'bar',
            'my'      => 'custom-api',
            'awesome' => 'it really is',
        );
    }

    return $data;
} );

/*
 {
    "namespace": "my-thing\/v1",
    "routes": {
        "\/my-thing\/v1": {
            "namespace": "my-thing\/v1",
            "methods": ["GET"],
            "endpoints": {
                "foo": "bar",
                "my": "custom-api",
                "awesome": "it really is"
            },
            "_links": {
                "self": "http:\/\/example.com\/wp-json\/my-thing\/v1"
            }
        },
        "\/my-thing\/v1\/awesome\/(?P<id>\\d+)": {
            "namespace": "my-thing\/v1",
            "methods": ["GET"],
            "endpoints": {
                "foo": "bar",
                "my": "custom-api",
                "awesome": "it really is"
            }
        }
    },
    "_links": {
        "up": [{
            "href": "http:\/\/example.com\/wp-json\/"
        }]
    }
}
*/

Leave a Comment