14+ years building on WordPress / Replies in under 5 hours
Development 12 min read · Updated July 2026

Create a WordPress Plugin Using Node.js: Complete Guide

Photo of Ajay Khandal
Ajay Khandal
WordPress Developer
How to Create a WordPress Plugin Using Node.js - Complete Guide
TL;DR

A WordPress plugin can call a Node.js backend by running an Express.js server on a separate port and using `wp_remote_get()` in PHP to make HTTP requests to it. The plugin PHP file registers shortcodes, calls the Node.js API, and caches responses using WordPress transients (`set_transient()` / `get_transient()`) to avoid hitting the server on every page load. The Node.js side runs Express.js, returns JSON, and can handle async operations (parallel API calls, WebSocket connections, CPU-intensive tasks) more efficiently than PHP. For production: use PM2 to keep the Node.js process running and auto-restart on crash; use Nginx as a reverse proxy to handle SSL termination; add API key or JWT authentication between WordPress and Node.js; implement rate limiting with `express-rate-limit`. This pattern is most useful for real-time data features, parallel third-party API calls, or integrations where a Node.js SDK is significantly better than the PHP equivalent — for standard API calls or CRUD operations, plain PHP is simpler.

WordPress plugins are PHP by default, but nothing stops you from running a Node.js process alongside WordPress and calling it from PHP over HTTP. This pattern is useful for specific use cases: real-time data that needs WebSocket connections, CPU-intensive processing that would block PHP-FPM, or integrations where a Node.js SDK exists but a solid PHP equivalent doesn’t. For a simple API call or a static data display, PHP alone is the right tool — this tutorial covers the cases where Node.js genuinely adds something.

The architecture: a PHP plugin file registers shortcodes and uses wp_remote_get() to call an Express.js server running on a separate port. The Node.js server handles the actual processing and returns JSON. WordPress caches the response with transients to avoid hitting the Node.js server on every page load. The two processes communicate over HTTP, stay independent, and can be deployed and scaled separately. If you’re managing PHP dependencies for the WordPress side of this, see the guide on using Composer for WordPress dependencies.

Why use Node.js for WordPress plugin development?

Node.js’s non-blocking I/O model makes it well-suited for specific tasks that PHP handles less gracefully:

  • Real-time features: WebSocket connections for live updates, chat, or streaming data — PHP can do this but Node.js handles concurrent connections more efficiently
  • Asynchronous parallel requests: Fetching from multiple third-party APIs simultaneously using Promise.all() without blocking
  • CPU-intensive processing: Image manipulation, data transformation, or computation that would tie up a PHP-FPM worker
  • Node.js-first SDK ecosystems: Some services (certain AI providers, payment systems, streaming platforms) have mature Node.js SDKs but thin PHP clients
  • Microservices: Isolating a specific feature so it can be scaled or replaced independently of the WordPress installation

When not to use this pattern: if you’re just displaying data from a REST API or doing standard WordPress CRUD operations, PHP handles that cleanly. The added complexity of running and maintaining a Node.js process is only worth it when the task genuinely benefits from it.

Prerequisites

Before starting, ensure you have the following installed:

  • WordPress 5.0+: On a local development server (Local by Flywheel, XAMPP, MAMP, or Docker)
  • Node.js v18+ (LTS): Download from nodejs.org
  • npm: Comes with Node.js
  • Basic familiarity with: JavaScript/Node.js, PHP, and WordPress plugin structure

Verify your setup:

node --version
npm --version
php --version

Step 1: Set up your plugin directory

Navigate to your WordPress plugins directory and create the plugin folder:

cd /path/to/wordpress/wp-content/plugins/
mkdir my-nodejs-plugin
cd my-nodejs-plugin

Your final directory structure will look like this:

my-nodejs-plugin/
├── my-nodejs-plugin.php    ← WordPress plugin entry point
├── server.js               ← Express.js server
├── package.json
├── .env
└── assets/
    └── css/
        └── style.css

Step 2: Create the WordPress plugin PHP file

This file is what WordPress recognizes as a plugin. It registers your shortcodes, handles HTTP communication with the Node.js server, and implements caching via WordPress transients.

<?php
/**
 * Plugin Name: My Node.js Plugin
 * Plugin URI: https://yourwebsite.com/my-nodejs-plugin
 * Description: A WordPress plugin backed by a Node.js server for real-time data processing
 * Version: 1.0.0
 * Author: Your Name
 * Author URI: https://yourwebsite.com
 * License: GPL v2 or later
 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain: my-nodejs-plugin
 * Requires at least: 5.0
 * Requires PHP: 7.4
 */

if (!defined('ABSPATH')) {
    exit;
}

define('MY_NODEJS_PLUGIN_VERSION', '1.0.0');
define('MY_NODEJS_PLUGIN_PATH', plugin_dir_path(__FILE__));
define('MY_NODEJS_PLUGIN_URL', plugin_dir_url(__FILE__));

/**
 * Fetch a message from the Node.js server
 */
function my_nodejs_fetch_message() {
    $args = array(
        'timeout' => 5,
        'headers' => array(
            'Content-Type' => 'application/json',
        ),
    );

    $response = wp_remote_get('http://localhost:3000/api/message', $args);

    if (is_wp_error($response)) {
        error_log('Node.js Plugin Error: ' . $response->get_error_message());
        return 'Unable to fetch data. Please try again later.';
    }

    $body = wp_remote_retrieve_body($response);
    $data = json_decode($body);

    if (isset($data->message)) {
        return esc_html($data->message);
    }

    return 'No data available.';
}

add_shortcode('node_message', 'my_nodejs_fetch_message');

/**
 * Fetch data from Node.js with WordPress transient caching.
 *
 * @param string $endpoint   Full URL of the Node.js API endpoint.
 * @param int    $cache_ttl  Cache duration in seconds (default: 5 minutes).
 * @return mixed             Decoded JSON object, or null on failure.
 */
function my_nodejs_fetch_data($endpoint, $cache_ttl = 300) {
    $cache_key = 'nodejs_' . md5($endpoint);
    $cached    = get_transient($cache_key);

    if ($cached !== false) {
        return $cached;
    }

    $response = wp_remote_get($endpoint, array(
        'timeout' => 5,
        'headers' => array('Content-Type' => 'application/json'),
    ));

    if (is_wp_error($response)) {
        error_log('Node.js API Error: ' . $response->get_error_message());
        return null;
    }

    $data = json_decode(wp_remote_retrieve_body($response));
    set_transient($cache_key, $data, $cache_ttl);

    return $data;
}

function my_nodejs_enqueue_scripts() {
    wp_enqueue_style(
        'my-nodejs-plugin-style',
        MY_NODEJS_PLUGIN_URL . 'assets/css/style.css',
        array(),
        MY_NODEJS_PLUGIN_VERSION
    );
}
add_action('wp_enqueue_scripts', 'my_nodejs_enqueue_scripts');

function my_nodejs_activate() {
    flush_rewrite_rules();
}
register_activation_hook(__FILE__, 'my_nodejs_activate');

function my_nodejs_deactivate() {
    flush_rewrite_rules();
}
register_deactivation_hook(__FILE__, 'my_nodejs_deactivate');
?>

Step 3: Set up the Node.js Express server

Initialize your Node.js project in the plugin directory:

npm init -y

Install dependencies:

npm install express cors body-parser dotenv
npm install --save-dev nodemon

Create server.js:

require('dotenv').config();
const express    = require('express');
const cors       = require('cors');
const bodyParser = require('body-parser');

const app  = express();
const PORT = process.env.PORT || 3000;

app.use(cors({
    origin: process.env.WORDPRESS_URL || 'http://localhost',
    credentials: true
}));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

app.use((req, res, next) => {
    console.log(`${new Date().toISOString()} - ${req.method} ${req.path}`);
    next();
});

app.get('/health', (req, res) => {
    res.json({
        status: 'ok',
        timestamp: new Date().toISOString(),
        uptime: process.uptime()
    });
});

app.get('/api/message', (req, res) => {
    try {
        res.json({
            message: 'Hello from Node.js in WordPress Plugin!',
            timestamp: new Date().toISOString()
        });
    } catch (error) {
        console.error('Error in /api/message:', error);
        res.status(500).json({ error: 'Internal server error' });
    }
});

app.post('/api/data', (req, res) => {
    try {
        const { data } = req.body;

        if (!data) {
            return res.status(400).json({ error: 'Data is required' });
        }

        res.json({
            received: data,
            processed: true,
            timestamp: new Date().toISOString()
        });
    } catch (error) {
        console.error('Error in /api/data:', error);
        res.status(500).json({ error: 'Internal server error' });
    }
});

app.use((err, req, res, next) => {
    console.error(err.stack);
    res.status(500).json({ error: 'Something went wrong!' });
});

app.use((req, res) => {
    res.status(404).json({ error: 'Endpoint not found' });
});

app.listen(PORT, () => {
    console.log(`Node.js server running on port ${PORT}`);
    console.log(`Health check: http://localhost:${PORT}/health`);
});

Create a .env file for configuration:

PORT=3000
WORDPRESS_URL=http://localhost
NODE_ENV=development

Add npm scripts to package.json:

{
  "name": "my-nodejs-plugin",
  "version": "1.0.0",
  "description": "WordPress plugin with Node.js backend",
  "main": "server.js",
  "scripts": {
    "start": "node server.js",
    "dev": "nodemon server.js"
  },
  "keywords": ["wordpress", "nodejs", "plugin"],
  "license": "GPL-2.0-or-later"
}

Step 4: How the WordPress–Node.js connection works

The connection happens through HTTP. When a visitor loads a page containing your shortcode:

  1. WordPress executes the shortcode function (PHP)
  2. PHP checks the WordPress transient cache — if data exists and is fresh, it returns the cached value immediately
  3. If cache miss, PHP makes an HTTP request to the Node.js server via wp_remote_get()
  4. Node.js processes the request and returns JSON
  5. PHP decodes the JSON, stores it in a transient, and returns the data to the shortcode

The 5-second timeout in wp_remote_get() is intentional — if the Node.js server is down, WordPress returns a graceful error message rather than hanging the page load. The transient cache (default: 5 minutes) means most page loads never hit the Node.js server at all.

Step 5: Test your plugin

Start the Node.js server in development mode:

npm run dev

You should see:

Node.js server running on port 3000
Health check: http://localhost:3000/health

Activate the plugin in WordPress (Plugins → Installed Plugins → Activate), then add [node_message] to any post or page. You should see “Hello from Node.js in WordPress Plugin!” rendered on the page.

Verify the Node.js endpoints directly:

  • http://localhost:3000/health — should return server status and uptime
  • http://localhost:3000/api/message — should return the JSON message

Real-world example: live cryptocurrency price display

Here’s a practical implementation: fetching live Bitcoin prices and displaying them via a WordPress shortcode. This uses the CoinGecko public API (free tier, 30 requests/minute limit — consider a CoinGecko API key for production).

Install Axios for HTTP requests:

npm install axios

Add the crypto endpoint to server.js:

const axios = require('axios');

/**
 * Bitcoin price endpoint via CoinGecko API.
 * Free tier: 30 req/min. Add x-cg-demo-api-key header for higher limits.
 */
app.get('/api/crypto', async (req, res) => {
    try {
        const response = await axios.get(
            'https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd&include_24hr_change=true',
            {
                timeout: 5000,
                headers: { 'Accept': 'application/json' }
            }
        );

        const bitcoin = response.data.bitcoin;
        res.json({
            currency: 'USD',
            price: '$' + bitcoin.usd.toLocaleString('en-US'),
            change_24h: bitcoin.usd_24h_change
                ? bitcoin.usd_24h_change.toFixed(2)
                : null,
            timestamp: new Date().toISOString()
        });
    } catch (error) {
        console.error('Crypto API Error:', error.message);
        res.status(500).json({
            error: 'Unable to fetch cryptocurrency data',
            message: error.message
        });
    }
});

/**
 * Multiple cryptocurrency prices in one request.
 */
app.get('/api/crypto/multiple', async (req, res) => {
    try {
        const response = await axios.get(
            'https://api.coingecko.com/api/v3/simple/price?ids=bitcoin,ethereum,litecoin&vs_currencies=usd&include_24hr_change=true',
            { timeout: 5000 }
        );

        res.json({
            ...response.data,
            timestamp: new Date().toISOString()
        });
    } catch (error) {
        console.error('Multiple Crypto Error:', error.message);
        res.status(500).json({ error: 'Failed to fetch data' });
    }
});

Add the WordPress shortcode to your PHP plugin file:

/**
 * Display live Bitcoin price — usage: [crypto_price]
 */
function my_nodejs_crypto_price() {
    $data = my_nodejs_fetch_data('http://localhost:3000/api/crypto', 60);

    if (!$data || isset($data->error)) {
        return '<p>Unable to fetch cryptocurrency price.</p>';
    }

    $change_html = '';
    if (isset($data->change_24h)) {
        $prefix      = $data->change_24h >= 0 ? '+' : '';
        $change_html = sprintf(
            '<p class="change">24h: %s%s%%</p>',
            $prefix,
            esc_html($data->change_24h)
        );
    }

    return sprintf(
        '<div class="crypto-price-widget">
            <h3>Bitcoin Price</h3>
            <p class="price">%s</p>
            %s
            <p class="updated">Updated: %s</p>
        </div>',
        esc_html($data->price),
        $change_html,
        esc_html($data->timestamp)
    );
}
add_shortcode('crypto_price', 'my_nodejs_crypto_price');

Add widget styles to assets/css/style.css:

.crypto-price-widget {
    background: #f8f9fa;
    border-left: 4px solid #007bff;
    padding: 20px;
    margin: 20px 0;
    border-radius: 4px;
}

.crypto-price-widget h3 {
    margin-top: 0;
    color: #333;
    font-size: 1.2em;
}

.crypto-price-widget .price {
    font-size: 2em;
    font-weight: bold;
    color: #28a745;
    margin: 10px 0;
}

.crypto-price-widget .updated {
    font-size: 0.9em;
    color: #6c757d;
}

.crypto-price-widget .change {
    font-size: 0.95em;
    color: #555;
}

Best practices for production

Security

  • API authentication: Implement JWT tokens or shared API keys for requests between WordPress and Node.js. WordPress nonces work well for requests originating from admin pages.
  • Input validation: Validate and sanitize all inputs on both sides — use esc_html() / sanitize_text_field() in PHP and validate with a schema library (Joi, Zod) in Node.js.
  • HTTPS in production: The WordPress-to-Node.js connection should go over HTTPS. Use Nginx as a reverse proxy to handle SSL termination.
  • Rate limiting: Use express-rate-limit to prevent abuse:
const rateLimit = require('express-rate-limit');

const limiter = rateLimit({
    windowMs: 15 * 60 * 1000, // 15 minutes
    max: 100
});

app.use('/api/', limiter);
  • CORS: Restrict the origin in your CORS config to your WordPress domain only — never use origin: '*' in production.

Performance

  • Transient cache: Already implemented — tune the TTL per endpoint (live prices: 60s, less volatile data: 300s+).
  • Response compression: Add gzip via the compression package:
const compression = require('compression');
app.use(compression());
  • Redis caching: For high-traffic sites, replace in-memory state in Node.js with Redis to persist cache across server restarts.

Process management and deployment

# Install PM2 globally
npm install -g pm2

# Start with PM2
pm2 start server.js --name "wordpress-nodejs-plugin"

# Persist across server reboots
pm2 startup
pm2 save

PM2 automatically restarts the process if it crashes and provides built-in logging. For production, add a health monitoring integration (New Relic, Datadog, or a simple uptime monitor) so you’re alerted if the Node.js process goes down.

Error handling and logging

  • Use Winston or Pino for structured logging (more useful than console.log in production)
  • WordPress side: the is_wp_error() check and graceful fallback string are the minimum — consider also caching a stale response and serving it when the Node.js server is unreachable
  • Monitor the Node.js server health endpoint (/health) with an uptime tool

Common issues and solutions

Node.js server won’t start

  • Check if port 3000 is in use: lsof -i :3000 (Mac/Linux) or netstat -ano | findstr :3000 (Windows)
  • Verify all dependencies installed: npm install
  • Ensure .env exists and is correctly formatted
  • Check Node.js version: node --version (v18+ required)

WordPress can’t connect to Node.js

  • Verify the server is running: curl http://localhost:3000/health
  • Check firewall — ensure port 3000 isn’t blocked between processes
  • Review CORS configuration (the origin must match your WordPress URL exactly)
  • Check WordPress debug logs: enable WP_DEBUG_LOG and look in wp-content/debug.log

Data not updating

  • WordPress transients are caching the old response — clear with: delete_transient('nodejs_' . md5($endpoint)), or reduce the TTL during development
  • The Node.js server may be returning cached data itself — add a cache-busting header or disable server-side caching in dev

Slow page loads

  • The transient cache means most requests never reach Node.js — check if your TTL is set correctly
  • If cache is cold and Node.js is slow: profile the specific endpoint with console.time() / console.timeEnd()
  • The 5-second wp_remote_get() timeout can slow page loads when Node.js is down — reduce the timeout and implement a stale-cache fallback for production

Plugin conflicts

  • Use unique function name prefixes (replace my_nodejs_ with something specific to your plugin) to avoid collisions with other plugins
  • Test with a default WordPress theme to isolate theme-related conflicts

If you’re building the frontend with React rather than PHP shortcodes, the companion guide on building a custom WordPress plugin with React JS covers the React-in-WordPress approach. For a full theme rebuild using React, see building a WordPress theme with React JS. And if you run into errors during development, the common WordPress errors guide covers the most frequent PHP and plugin-related issues you’ll encounter.

Frequently asked questions

Yes. A WordPress plugin can call a Node.js server by running Express.js on a separate port and using PHP's `wp_remote_get()` function to make HTTP requests to it. WordPress handles the frontend (shortcodes, admin UI, content management) while Node.js handles specific backend processing (real-time data, CPU-intensive tasks, async API calls). The two processes communicate over HTTP and operate independently. This requires a hosting environment that can run both PHP and Node.js processes — shared hosting typically doesn't support this, but VPS hosting or managed WordPress hosts that allow Node.js do.

No. This architecture requires a self-hosted WordPress.org installation where you can install custom plugins and run server-side Node.js processes. WordPress.com hosted plans don't allow custom plugin installation or arbitrary server-side code execution. You need either a self-hosted VPS (DigitalOcean, Linode, Hetzner), a managed WordPress host that supports Node.js processes alongside PHP, or a cloud platform (AWS, GCP, Azure) where you can run both PHP and Node.js services.

Use PM2, a Node.js process manager that automatically restarts the process if it crashes and persists it across server reboots. Install it globally with `npm install -g pm2`, start your server with `pm2 start server.js --name 'my-plugin'`, then run `pm2 startup` and `pm2 save` to configure auto-start. PM2 also provides built-in log management and a monitoring dashboard. For the web-facing layer, use Nginx as a reverse proxy in front of your Node.js server to handle SSL/TLS termination, request rate limiting at the network level, and load balancing if you run multiple Node.js instances.

Use multiple layers: (1) Shared API key — generate a random secret, store it in your .env file and in WordPress options, and require it as a header on every request (check it in an Express.js middleware). (2) HTTPS — in production, all communication should go over HTTPS; use Nginx as a reverse proxy to handle SSL/TLS. (3) CORS — restrict the origin in your Express.js CORS configuration to your WordPress domain only, never use `origin: '*'` in production. (4) Rate limiting — use the `express-rate-limit` package to prevent abuse. (5) Input validation — sanitize all inputs in both PHP (`esc_html()`, `sanitize_text_field()`) and Node.js (Joi or Zod for schema validation).

Yes. TypeScript adds type safety and better IDE support, which is particularly useful for larger plugin backends. Install TypeScript with `npm install --save-dev typescript @types/node @types/express`, create a `tsconfig.json` targeting your Node.js version (e.g., `"target": "ES2020", "module": "commonjs"`), write your server in `.ts` files, and add a build step to `package.json` scripts: `"build": "tsc"` and `"start": "node dist/server.js"`. In development, `ts-node-dev` or `tsx` runs TypeScript directly without a separate compile step (replaces `nodemon`).

You need an environment that supports both PHP (for WordPress) and the ability to run a persistent Node.js process. Options: VPS hosting (DigitalOcean, Hetzner, Linode — starting at £4–8/month, you manage the server), cloud platforms (AWS EC2, Google Compute Engine, Azure VM — more flexible, pay-per-use), or managed WordPress hosts that allow SSH access and process management. Shared hosting won't work — it doesn't allow persistent background processes. For a basic VPS setup: install WordPress (or use a one-click image), install Node.js via nvm, configure Nginx as a reverse proxy, and use PM2 for process management.

Photo of Ajay Khandal

Written by Ajay Khandal

I'm a freelance WordPress developer with 14+ years of experience building, fixing, and speeding up sites for businesses, agencies, and store owners across the US, UK, Europe, and Australia. I specialize in custom themes, WooCommerce, and performance — the kind of work that shows up as faster load times and fewer support tickets. No account managers, no outsourced tickets — you work directly with me, with replies typically inside 5 hours.

Work with me →