Roots Discourse

Custom endpoints for charts appearing inside a post


I have the following scenario, I have created some custom tables in my database that I would like to make available as secure endpoints that return JSON (simple GET endpoints like you would easily make in Laravel) so I can consume them using Charts.js for Interactive Charts to appear inside my posts. I was thinking of just having specific JS written for each post.

I’ve come across this article ( but was curious if there was a recommended approach for doing such a thing better utilising Sage’s functionality?

Many thanks

That article is pretty much what I would have done. The permission checks are done in the register_rest_route. For WP request to the REST API, you’d have to pass a nonce to the header or you can use a plugin to use passwords.

Thank you for your response, are you able to perhaps point me to a reference on the WP request to the REST API via passing a nonce to the header?

Sure, there’s several files you gotta modify and I will be using demyx as my namespace.

Localizing a global array variable that can be accessible anywhere on the frontend. For this example, I’ve included the REST url endpoint and nonce; accessible via and demyx.nonce in your .js files.

 * Theme assets
add_action('wp_enqueue_scripts', function () {
    wp_enqueue_style('sage/main.css', asset_path('styles/main.css'), false, null);
    wp_enqueue_script('sage/main.js', asset_path('scripts/main.js'), ['jquery'], null, true);
    wp_localize_script('sage/main.js', 'demyx', [
        'rest'  => home_url() . '/wp-json/demyx/v1',
        'nonce' => wp_create_nonce('wp_rest'),

    if (is_single() && comments_open() && get_option('thread_comments')) {
}, 100);

Must add your global variable, otherwise webpack will say demyx is not defined.

module.exports = {
  'root': true,
  'extends': 'eslint:recommended',
  'globals': {
    'wp': true,
    'demyx': true,

I like to put specific functionalities of WordPress into their own files.


namespace App;
use WP_Query;
use WP_Error;
 *      All of WP REST access is only available to logged in users (aka you)
add_filter('rest_authentication_errors', function($result) {
    if (!empty( $result )) {
        return $result;
    if (! is_user_logged_in()) {
        return new WP_Error('rest_not_logged_in', 'You are not currently logged in.', array( 'status' => 401));
    return $result;
 *      REST endpoints
add_action('rest_api_init', function() {
    *      Custom endpoint
    *      https://domain.tld/wp-json/demyx/v1/custom
    register_rest_route('demyx/v1', '/custom', array(
        'methods' => ['GET'],
        'callback' => function() {
            // Gets data from the ajax request
            $get_custom_data    = (empty($_GET['custom_data']) ) ? null : sanitize_text_field($_GET['custom_data']);
            $data               = [];
            // Do your wp_query() or custom PHP query here

            return $data;
        'permission_callback' => function () {
            // Permission check, returns a boolean
            return current_user_can('edit_others_posts');

Include rest.php into the fold.

 * Sage required files
 * The mapped array determines the code library included in your theme.
 * Add or remove files to the array as needed. Supports child theme overrides.
array_map(function ($file) use ($sage_error) {
    $file = "../app/{$file}.php";
    if (!locate_template($file, true, true)) {
        $sage_error(sprintf(__('Error locating <code>%s</code> for inclusion.', 'sage'), $file), 'File not found');
    }, [

I’m placing this in common.js as an example. Feel free to put this any file of your choosing. Probably have to make blog.js to fire in only blog posts?

export default {
  init() {
    // JavaScript to be fired on all pages
      url: + '/custom',
      method: 'GET',
      data: {
        custom_data: $('input').val(),
      beforeSend: function (xhr) {
        xhr.setRequestHeader('X-WP-Nonce', demyx.nonce);
      success: function(data) {
  finalize() {
    // JavaScript to be fired on all pages, after page specific JS is fired

Let me know if I’ve missed anything.