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\/"
}]
}
}
*/