How to securely integrate Shopify headless API endpoints into WordPress custom plugins using Rewrite API custom endpoints
Leveraging WordPress Rewrite API for Secure Shopify Headless Integration
Integrating external APIs into WordPress, especially for e-commerce functionalities like those offered by Shopify’s headless API, requires a robust and secure approach. While direct AJAX calls are common, they often expose sensitive endpoints and can lead to cross-origin issues or security vulnerabilities. This guide details how to use WordPress’s built-in Rewrite API to create custom endpoints that act as secure proxies for your Shopify headless API calls, enhancing both security and maintainability within your custom WordPress plugins.
Understanding the WordPress Rewrite API
The WordPress Rewrite API is primarily used for managing permalinks and creating custom URL structures. However, its core functionality of intercepting requests and mapping them to specific PHP callbacks is exceptionally well-suited for building custom API endpoints. By defining custom rewrite rules and associated query variables, we can hook into WordPress’s request processing flow to handle incoming requests, perform necessary actions (like fetching data from Shopify), and return the results.
Plugin Structure and Initialization
We’ll create a simple WordPress plugin to house our custom endpoints. This plugin will register the necessary rewrite rules and the callback functions that will handle the API requests.
Create a new directory in your WordPress plugins folder, e.g., wp-content/plugins/shopify-headless-proxy/. Inside this directory, create your main plugin file, shopify-headless-proxy.php.
Main Plugin File: shopify-headless-proxy.php
<?php
/**
* Plugin Name: Shopify Headless Proxy
* Description: Securely proxies Shopify headless API requests.
* Version: 1.0
* Author: Antigravity
*/
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// Define plugin constants.
define( 'SHOPIFY_HEADLESS_PROXY_VERSION', '1.0' );
define( 'SHOPIFY_API_ENDPOINT', 'https://your-shop-name.myshopify.com/api/2023-10/graphql.json' ); // Replace with your actual GraphQL endpoint
define( 'SHOPIFY_API_KEY', 'your-api-key' ); // Replace with your actual API key
define( 'SHOPIFY_API_PASSWORD', 'your-api-password' ); // Replace with your actual API password
// Include necessary files.
require_once plugin_dir_path( __FILE__ ) . 'includes/class-shopify-headless-proxy-api.php';
// Instantiate the API handler.
function register_shopify_headless_proxy_api() {
$api_handler = new Shopify_Headless_Proxy_API();
$api_handler->register_endpoints();
}
add_action( 'plugins_loaded', 'register_shopify_headless_proxy_api' );
// Flush rewrite rules on plugin activation/deactivation.
register_activation_hook( __FILE__, array( 'Shopify_Headless_Proxy_API', 'flush_rewrite_rules' ) );
register_deactivation_hook( __FILE__, array( 'Shopify_Headless_Proxy_API', 'flush_rewrite_rules' ) );
?>
Implementing the API Handler Class
Create a new directory includes/ inside your plugin folder. Inside includes/, create the file class-shopify-headless-proxy-api.php. This class will manage the rewrite rules, query variables, and the actual API request handling.
API Handler Class: includes/class-shopify-headless-proxy-api.php
<?php
/**
* Shopify Headless Proxy API Handler.
*/
class Shopify_Headless_Proxy_API {
/**
* Constructor.
*/
public function __construct() {
// Hook into WordPress to add rewrite rules and query vars.
add_action( 'init', array( $this, 'add_rewrite_rules' ) );
add_filter( 'query_vars', array( $this, 'add_query_vars' ) );
add_action( 'parse_request', array( $this, 'handle_api_request' ) );
}
/**
* Registers custom rewrite rules for our API endpoints.
*/
public function add_rewrite_rules() {
// Example: Proxy for fetching products.
// This rule will match URLs like /api/shopify/products/
add_rewrite_rule(
'^api/shopify/products/?$', // Regex for the URL pattern
'index.php?shopify_api_action=products', // Query vars to set
'top' // Position in the rewrite rules (top means it's checked first)
);
// Example: Proxy for fetching a single product by ID.
// This rule will match URLs like /api/shopify/products/(\d+)/
add_rewrite_rule(
'^api/shopify/products/(\d+)/?$',
'index.php?shopify_api_action=single_product&product_id=$matches1',
'top'
);
// Add more rules for other Shopify API endpoints as needed.
// For example, for collections:
// add_rewrite_rule(
// '^api/shopify/collections/?$',
// 'index.php?shopify_api_action=collections',
// 'top'
// );
}
/**
* Adds custom query variables to WordPress.
*
* @param array $vars Existing query variables.
* @return array Modified query variables.
*/
public function add_query_vars( $vars ) {
$vars[] = 'shopify_api_action';
$vars[] = 'product_id';
// Add more query vars for other actions and parameters.
return $vars;
}
/**
* Handles incoming requests and routes them to the appropriate API action.
*
* @param WP_Query $wp_query The current WP_Query object.
*/
public function handle_api_request( $wp_query ) {
// Check if our custom query variables are set.
if ( ! isset( $wp_query->query_vars['shopify_api_action'] ) ) {
return; // Not our endpoint, let WordPress handle it.
}
// Prevent WordPress from loading the theme and template.
$wp_query->is_404 = false;
$wp_query->is_page = true; // Treat as a page to prevent theme loading
$wp_query->is_singular = true;
$wp_query->is_home = false;
$wp_query->is_archive = false;
$wp_query->is_category = false;
$wp_query->is_tag = false;
$wp_query->is_tax = false;
$wp_query->is_single = false;
$wp_query->is_post_type_archive = false;
$wp_query->is_feed = false;
$wp_query->is_comment_feed = false;
$wp_query->is_trackback = false;
$wp_query->is_search = false;
$wp_query->is_paged = false;
$wp_query->is_robots = false;
$wp_query->is_posts_page = false;
$wp_query->is_attachment = false;
$wp_query->is_comments_popup = false;
$wp_query->is_admin = false; // Crucial to prevent admin context
// Set the response header for JSON content.
header( 'Content-Type: application/json' );
// Get the action and any associated parameters.
$action = $wp_query->get( 'shopify_api_action' );
$product_id = $wp_query->get( 'product_id' );
// Dispatch to the appropriate handler method.
switch ( $action ) {
case 'products':
$this->get_shopify_products();
break;
case 'single_product':
if ( $product_id ) {
$this->get_shopify_single_product( $product_id );
} else {
$this->send_error_response( 'Product ID is missing.', 400 );
}
break;
// Add cases for other actions (e.g., 'collections').
default:
$this->send_error_response( 'Unknown Shopify API action.', 400 );
break;
}
// Stop WordPress from further processing.
exit;
}
/**
* Fetches products from Shopify's GraphQL API.
*/
private function get_shopify_products() {
$query = '
query {
products(first: 10) {
edges {
node {
id
title
handle
descriptionHtml
images(first: 1) {
edges {
node {
url
altText
}
}
}
variants(first: 1) {
edges {
node {
priceV2 {
amount
currencyCode
}
}
}
}
}
}
}
}
';
$response = $this->make_shopify_request( $query );
if ( is_wp_error( $response ) ) {
$this->send_error_response( 'Failed to fetch products from Shopify.', 500, $response->get_error_message() );
}
$data = json_decode( $response['body'], true );
if ( isset( $data['errors'] ) ) {
$this->send_error_response( 'Shopify API returned errors.', 500, $data['errors'] );
}
// Process and format the data as needed.
$formatted_products = [];
if ( isset( $data['data']['products']['edges'] ) ) {
foreach ( $data['data']['products']['edges'] as $edge ) {
$node = $edge['node'];
$formatted_products[] = [
'id' => $node['id'],
'title' => $node['title'],
'handle' => $node['handle'],
'descriptionHtml' => $node['descriptionHtml'],
'imageUrl' => $node['images']['edges'][0]['node']['url'] ?? null,
'price' => $node['variants']['edges'][0]['node']['priceV2']['amount'] ?? null,
'currency' => $node['variants']['edges'][0]['node']['priceV2']['currencyCode'] ?? null,
];
}
}
wp_send_json_success( $formatted_products );
}
/**
* Fetches a single product by ID from Shopify's GraphQL API.
*
* @param string $product_id The Shopify product ID (e.g., 'gid://shopify/Product/1234567890').
*/
private function get_shopify_single_product( $product_id ) {
// Ensure the product ID is in the correct GraphQL global ID format if necessary.
// WordPress rewrite rules might capture just the numeric ID.
// We need to prepend 'gid://shopify/Product/' if that's how Shopify expects it.
// For simplicity, assuming the rewrite rule captures the full GID or we can construct it.
// If your rewrite rule captures only the numeric ID, you might need to adjust this.
$graphql_product_id = $product_id;
if ( ! str_starts_with( $product_id, 'gid://shopify/Product/' ) ) {
$graphql_product_id = 'gid://shopify/Product/' . $product_id;
}
$query = sprintf( '
query {
product(id: "%s") {
id
title
handle
descriptionHtml
images(first: 5) {
edges {
node {
url
altText
}
}
}
variants(first: 10) {
edges {
node {
id
title
priceV2 {
amount
currencyCode
}
selectedOptions {
name
value
}
}
}
}
}
}
', esc_attr( $graphql_product_id ) );
$response = $this->make_shopify_request( $query );
if ( is_wp_error( $response ) ) {
$this->send_error_response( 'Failed to fetch product from Shopify.', 500, $response->get_error_message() );
}
$data = json_decode( $response['body'], true );
if ( isset( $data['errors'] ) ) {
$this->send_error_response( 'Shopify API returned errors.', 500, $data['errors'] );
}
if ( ! isset( $data['data']['product'] ) || $data['data']['product'] === null ) {
$this->send_error_response( 'Product not found.', 404 );
}
wp_send_json_success( $data['data']['product'] );
}
/**
* Makes a request to the Shopify GraphQL API.
*
* @param string $query The GraphQL query string.
* @return array|WP_Error The response from the API or a WP_Error object.
*/
private function make_shopify_request( $query ) {
$api_endpoint = SHOPIFY_API_ENDPOINT;
$api_key = SHOPIFY_API_KEY;
$api_password = SHOPIFY_API_PASSWORD;
// Basic Authentication for Shopify Admin API
$auth_string = base64_encode( "{$api_key}:{$api_password}" );
$args = array(
'method' => 'POST',
'timeout' => 30,
'headers' => array(
'Content-Type' => 'application/json',
'X-Shopify-Storefront-Access-Token' => 'YOUR_STOREFRONT_ACCESS_TOKEN', // Use Storefront API token for public data
// For Admin API, use Basic Auth:
// 'Authorization' => 'Basic ' . $auth_string,
),
'body' => json_encode( array( 'query' => $query ) ),
);
// IMPORTANT: For public data (products, collections), use the Storefront API.
// You'll need a Storefront API access token. Replace 'YOUR_STOREFRONT_ACCESS_TOKEN' above.
// If you need to access private data (orders, customers), you'll need the Admin API
// and use the 'Authorization: Basic ...' header instead of 'X-Shopify-Storefront-Access-Token'.
// Ensure your Shopify app has the correct permissions.
$response = wp_remote_request( $api_endpoint, $args );
return $response;
}
/**
* Sends a JSON error response.
*
* @param string $message The error message.
* @param int $status_code The HTTP status code.
* @param mixed $details Optional details about the error.
*/
private function send_error_response( $message, $status_code = 400, $details = null ) {
header( 'Content-Type: application/json', true, $status_code );
$response = array(
'success' => false,
'message' => $message,
);
if ( $details ) {
$response['details'] = $details;
}
wp_send_json( $response, $status_code );
exit;
}
/**
* Flushes rewrite rules.
* This static method is called on plugin activation/deactivation.
*/
public static function flush_rewrite_rules() {
// Ensure rewrite rules are flushed when the plugin is activated or deactivated.
flush_rewrite_rules();
}
}
?>
Configuration and Security Considerations
Before activating the plugin, ensure you have configured the constants at the top of shopify-headless-proxy.php:
SHOPIFY_API_ENDPOINT: This should be your Shopify GraphQL endpoint. For public data (products, collections), this is typically your Storefront API endpoint. For private data (orders, customers), it’s your Admin API endpoint.SHOPIFY_API_KEYandSHOPIFY_API_PASSWORD: These are your Shopify API credentials. For the Storefront API, you’ll use a Storefront API access token. For the Admin API, you’ll use your API key and password.
Crucially, for accessing public product data, you should use the Shopify Storefront API. This requires a Storefront API access token, which is different from Admin API credentials. In the make_shopify_request method, ensure you are using the correct header: X-Shopify-Storefront-Access-Token with your Storefront API token. If you intend to access private data (like orders), you would switch to the Admin API endpoint and use the Authorization: Basic ... header.
Security Best Practices:
- Never expose sensitive API keys or passwords directly in client-side JavaScript. This plugin acts as a server-side proxy, keeping these credentials secure within your WordPress environment.
- Use environment variables or a secure configuration management system for storing API keys and passwords in a production environment, rather than hardcoding them directly in the plugin file.
- Implement proper authentication and authorization if your API endpoints are intended for specific user roles or authenticated users within WordPress.
- Rate Limiting: Be mindful of Shopify’s API rate limits. For high-traffic sites, consider implementing caching mechanisms or rate limiting on your WordPress endpoints.
- Input Validation: Always validate and sanitize any input received from the client before using it in API requests (e.g., product IDs, search queries).
Activation and Testing
1. **Save your files:** Ensure all files are saved in their correct locations.
2. **Activate the plugin:** Go to your WordPress admin dashboard, navigate to “Plugins,” and activate “Shopify Headless Proxy.”
3. **Flush Rewrite Rules:** Because we’ve added rewrite rules, you need to flush them. This is handled automatically by the `register_activation_hook` and `register_deactivation_hook` in the main plugin file. If you encounter issues, you can manually flush them by going to Settings > Permalinks and clicking “Save Changes.”
4. **Test the endpoints:** You can now test your custom endpoints using tools like curl or Postman:
Fetching all products:
curl https://your-wordpress-site.com/api/shopify/products/
Fetching a single product (replace ‘1234567890’ with an actual product ID or handle if your rule is adjusted):
curl https://your-wordpress-site.com/api/shopify/products/1234567890/
If everything is configured correctly, you should receive a JSON response from Shopify, proxied through your WordPress site.
Extending the Proxy
This example provides a foundation for proxying Shopify’s product data. You can extend this pattern to:
- Proxy other Shopify API endpoints (collections, customers, orders, etc.).
- Implement more sophisticated data transformation and filtering before sending data to the client.
- Add authentication checks to ensure only authorized users can access certain endpoints.
- Integrate with WordPress’s caching mechanisms (e.g., Transients API) to reduce direct calls to Shopify and improve performance.
- Handle different HTTP methods (POST, PUT, DELETE) for actions like creating or updating data, if your use case requires it and your Shopify API access allows it.
By utilizing the WordPress Rewrite API, you create a clean, maintainable, and secure way to integrate external services like Shopify headless into your WordPress ecosystem, abstracting away direct API complexities and enhancing your application’s architecture.