---
description: https://fancysquares.blog/add-a-custom-endpoint-to-wordpress-rest-api
---

# \[API] Add a custom endpoint to WordPress REST API with Pagination



## Add a custom endpoint to WordPress REST API with Pagination

Published: October 18th 2019

Are you trying to add a custom endpoint and/or route to the WordPress REST API?

Are you then stuck in a rut if its missing pagination?

Have no fear, I got your back! (full code is the bottom before that are small explanations explaining what’s going on)

### ADDING A CUSTOM ENDPOINT TO WORDPRESS REST API

We are going to start with extending `WP_REST_CONTROLLER`class REFINED\_POST\_ENDPOINT extends WP\_REST\_Controller { } add\_action( 'rest\_api\_init', function () { $controller = new REFINED\_POST\_ENDPOINT(); $controller->register\_routes(); } );

In `REFINED_POST_ENDPOINT` is where we’ll place code specifics and the `add_action` is what will run all the little magical things we do (you added code, and route, this will run it).

In our newly created **class** we will start with the following constructor:/\*\*\* Constructor. \*/public function \_\_construct() { $this->namespace = 'rmh/v1'; $this->rest\_base = 'posts'; $this->post\_type = 'post';}

From top to bottom:

* rest base is set: **yourdomain.com/wp-json/rmh/v1** (or whatever your heart desires!)
* next is the base for the endpoint itself **“posts”**, not to be confused with the post type.  (**yourdomain.com/wp-json/rmh/v1/posts**)
* last is the post type we are getting.  In this example, we are grabbing **“post”**

### REGISTERING YOUR NEW ROUTE!

/\*\* \* Register the component routes. \*/ public function register\_routes() { register\_rest\_route( $this->namespace, '/' . $this->rest\_base, array( array( 'methods' => WP\_REST\_Server::READABLE, 'callback' => array( $this, 'get\_items' ), 'permission\_callback' => array( $this, 'get\_items\_permissions\_check' ), 'args' => $this->get\_collection\_params(), ) ) ); }

From top to bottom:

* We are combing your namespace and base together
* making the request **READABLE** so to be clear that means this is a get request ([see here for me details](https://developer.wordpress.org/reference/classes/wp_rest_server/))
* we will then get then return the code placed in our `get_items` function
* it then checks to make sure we have permission to do so through our `get_items_permissions_check`
* and it checks what are the possible arguments that can be used with it `get_collection_params()`

### RETRIEVING POSTS AND SETTING RESPONSE HEADERS FOR PAGINATION (X-WP-TOTAL, X-WP-TOTALPAGES)

/\*\* \* Retrieve posts. \*/ public function get\_items( $request ) { $args = array( 'post\_type' => $this->post\_type, 'posts\_per\_page' => $request\['per\_page'], 'paged' => $request\['page'], //'name' => $request\['slug'], ); // use WP\_Query to get the results with pagination $query = new WP\_Query( $args ); // if no posts found return if( empty($query->posts) ){ return new WP\_Error( 'no\_posts', \_\_('No post found'), array( 'status' => 404 ) ); } // set max number of pages and total num of posts $posts = $query->posts; $max\_pages = $query->max\_num\_pages; $total = $query->found\_posts; foreach ( $posts as $post ) { $response = $this->prepare\_item\_for\_response( $post, $request ); $data\[] = $this->prepare\_response\_for\_collection( $response ); } // set headers and return response $response = new WP\_REST\_Response($data, 200); $response->header( 'X-WP-Total', $total ); $response->header( 'X-WP-TotalPages', $max\_pages ); return $response; }/\*\* \* Check if a given request has access to post items. \*/ public function get\_items\_permissions\_check( $request ) { return true; }/\*\* \* Get the query params for collections \*/ 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', ), ); }/\*\* \* Prepares post data for return as an object. \*/public function prepare\_item\_for\_response( $post, $request ) { $data = array( 'name' => $post->post\_title, 'copy' => $post->post\_content, 'featured\_img' => get\_the\_post\_thumbnail\_url($post->ID, 'full'), 'external\_link' => get\_field('external\_post\_link',$post->ID), 'date' => date( "F j, Y", strtotime($post->post\_date)) ); return $data;}

From top to bottom `get_items`:

* We are setting are variables for `WP_Query`
  * why that instead of **get\_posts**? Because we want to return the total number of pages and posts with it as well
* We then set the variables
  * If nothing then let us know
* Set the variables for what we will loop through, total number pages and total number of posts found
* Loop through the posts that will check the data we set and return it in the response
* Finish `get_items` up by returning the response and setting the response headers for pagination
* We then set the permission check
* For `get_collection_params` we are setting the `page` and `per_page` details and defaults
* For `prepare_item_for_response` we are setting the data we literally returning per post and is called above in `get_items`
  * You, of course, would set this based on your needs.
  * In this example, we are getting the title/content/featured image/an ACF Field/date

```
class REFINED_POST_ENDPOINT extends WP_REST_Controller {
    /**
   * Constructor.
   */
  public function __construct() {
    $this->namespace = 'rmh/v1';
    $this->rest_base = 'posts';
        $this->post_type = 'post';
  }

  /**
   * Register the component routes.
   */
  public function register_routes() {
    register_rest_route( $this->namespace, '/' . $this->rest_base, array(
      array(
        'methods'             => WP_REST_Server::READABLE,
        'callback'            => array( $this, 'get_items' ),
        'permission_callback' => array( $this, 'get_items_permissions_check' ),
        'args'                => $this->get_collection_params(),
      )
    ) );
  }
  
  /**
   * Retrieve posts.
   */
  public function get_items( $request ) {
    $args = array(
      'post_type'      => $this->post_type,
      'posts_per_page' => $request['per_page'],
      'paged'           => $request['page'],
      //'name' => $request['slug'],
    );
  
    // use WP_Query to get the results with pagination
    $query = new WP_Query( $args ); 

    // if no posts found return 
    if( empty($query->posts) ){
      return new WP_Error( 'no_posts', __('No post found'), array( 'status' => 404 ) );
    }


    // set max number of pages and total num of posts
    $posts = $query->posts;
    
    $max_pages = $query->max_num_pages;
    $total = $query->found_posts;

    foreach ( $posts as $post ) {
      $response = $this->prepare_item_for_response( $post, $request );
      $data[] = $this->prepare_response_for_collection( $response );  
    }

  
    // set headers and return response      
    $response = new WP_REST_Response($data, 200);

    $response->header( 'X-WP-Total', $total ); 
    $response->header( 'X-WP-TotalPages', $max_pages );

    return $response;
  }
  
  /**
   * Check if a given request has access to post items.
   */
  public function get_items_permissions_check( $request ) {
    return true;
  }
  
  /**
   * Get the query params for collections
   */
  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',              
      ),
    );
  }
  
  /**
   * Prepares post data for return as an object.
   */
  public function prepare_item_for_response( $post, $request ) {

    $data = array(
      'name'    => $post->post_title,
      'copy' => $post->post_content,
      'featured_img' => get_the_post_thumbnail_url($post->ID, 'full'),
      'external_link' => get_field('external_post_link',$post->ID),    
      'date' => date( "F j, Y", strtotime($post->post_date))
    );
  
    return $data;
  }
}





add_action( 'rest_api_init', function () {
  $controller = new REFINED_POST_ENDPOINT();
  $controller->register_routes();
} );
```

Now you’ll have the following:

* `yourdomain.com/wp-json/rmh/v1/posts`
* `yourdomain.com/wp-json/rmh/v1/posts?page=2` etc
* `yourdomain.com/wp-json/rmh/v1/posts?per_page=3` etc
* `yourdomain.com/wp-json/rmh/v1/posts?per_page=5&page=3` and so on!
