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:
- WordPress executes the shortcode function (PHP)
- PHP checks the WordPress transient cache — if data exists and is fresh, it returns the cached value immediately
- If cache miss, PHP makes an HTTP request to the Node.js server via
wp_remote_get() - Node.js processes the request and returns JSON
- 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 uptimehttp://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-limitto 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
originin your CORS config to your WordPress domain only — never useorigin: '*'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
compressionpackage:
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.login 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) ornetstat -ano | findstr :3000(Windows) - Verify all dependencies installed:
npm install - Ensure
.envexists 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
originmust match your WordPress URL exactly) - Check WordPress debug logs: enable
WP_DEBUG_LOGand look inwp-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.


