DEV Community

Maksym
Maksym

Posted on

Creating a Custom Shopify App with Node.js

Building a Custom Shopify App with Node.js and Express (OAuth Flow)

This guide provides a foundational walkthrough for setting up a custom private Shopify application using Node.js and the Express framework. We'll focus on implementing the OAuth 2.0 flow to securely obtain an access token, which is essential for making authenticated requests to the Shopify Admin API.


Prerequisites

Before you begin, make sure you have the following:

  • A Shopify Partner Account: You can sign up for free at Shopify Partners.
  • A Development Store: Create a development store from your Shopify Partner dashboard. This will be the sandbox for testing your app.
  • Node.js and npm/pnpm/yarn: Ensure you have a recent version of Node.js and a package manager installed on your system.
  • A Tunneling Service (e.g., ngrok): Shopify requires all apps to be served over HTTPS. A tool like ngrok creates a secure, public tunnel to your local development server.

Setting Up Your Node.js Project

1. Create and Initialize the Project

Create a new directory for your app and initialize a Node.js project.

mkdir my-shopify-app
cd my-shopify-app
npm init -y
Enter fullscreen mode Exit fullscreen mode

2. Install Dependencies

We'll use express for the web server, dotenv to manage environment variables, axios for making HTTP requests, and crypto (a built-in Node module) for HMAC validation.

npm install express dotenv axios
Enter fullscreen mode Exit fullscreen mode

3. Create the Main Application File

touch index.js
Enter fullscreen mode Exit fullscreen mode

Creating Your App in the Shopify Partner Dashboard

  1. Log in to your Shopify Partner Dashboard.
  2. Navigate to Apps and click Create app.
  3. Choose Custom app (or App if you plan to eventually list it).
  4. Give your app a name (e.g., "My Custom App").
  5. Set the App URL to a placeholder, like http://localhost:3000. You will update this later with your ngrok URL.
  6. Set the Allowed redirection URL(s) to http://localhost:3000/shopify/callback.
  7. After creating the app, go to App details (or App setup), then API credentials. Note down the API key (Client ID) and API secret key (Client Secret). You'll need these to configure your application.

Configuring Environment Variables

Create a file named .env in the root of your project to store your credentials securely.

SHOPIFY_API_KEY=your_api_key
SHOPIFY_API_SECRET=your_api_secret_key
# Use the latest stable API version (e.g., 2024-04). Check the Shopify documentation.
SHOPIFY_API_VERSION=2024-04
SHOPIFY_SCOPES=read_products,write_products 
HOST=http://localhost:3000
PORT=3000 
Enter fullscreen mode Exit fullscreen mode
  • Replace your_api_key and your_api_secret_key with the values from your Shopify app settings.
  • SHOPIFY_SCOPES defines the permissions your app will request from the merchant.
  • HOST is the base URL of your app. We'll use this with the ngrok public URL later.

Building the Node.js Server (index.js)

This code sets up the Express server and handles the two main steps of the Shopify OAuth flow: the Installation Route and the Callback Route.

// index.js

// Import required packages
const express = require('express');
const crypto = require('crypto');
const axios = require('axios');
require('dotenv').config();

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

// Get credentials from environment variables
const {
  SHOPIFY_API_KEY,
  SHOPIFY_API_SECRET,
  SHOPIFY_API_VERSION,
  SHOPIFY_SCOPES,
  HOST
} = process.env;

// In a production app, this should be a secure, persistent database.
// For this simple example, we use an in-memory object to store access tokens.
const shops = {}; 

// 1. Installation Route: Redirects to the Shopify authorization screen
app.get('/shopify', (req, res) => {
  const shop = req.query.shop;
  if (!shop) {
    return res.status(400).send('Missing shop parameter. Please add ?shop=your-development-shop.myshopify.com to your request.');
  }

  // Generate a random state value to prevent CSRF attacks
  const state = crypto.randomBytes(16).toString('hex');
  const redirectUri = `${HOST}/shopify/callback`;

  // Construct the Shopify authorization URL
  const installUrl = `https://${shop}/admin/oauth/authorize?client_id=${SHOPIFY_API_KEY}&scope=${SHOPIFY_SCOPES}&state=${state}&redirect_uri=${redirectUri}`;

  // NOTE: In a real app, you would save the 'state' value temporarily for validation in the callback.

  res.redirect(installUrl);
});

// 2. Callback Route: Exchanges the authorization code for a permanent access token
app.get('/shopify/callback', async (req, res) => {
  const { shop, hmac, code } = req.query;

  // --- HMAC Validation (Security Check) ---
  const map = { ...req.query };
  delete map.hmac; // The hmac itself is not part of the message we validate

  // Sort and serialize the parameters to generate the message string
  const message = new URLSearchParams(map).toString(); 

  const providedHmac = Buffer.from(hmac, 'utf-8');

  // Generate the hash using the secret key
  const generatedHash = Buffer.from(
    crypto.createHmac('sha256', SHOPIFY_API_SECRET)
      .update(message)
      .digest('hex'),
    'utf-8'
  );

  // Use crypto.timingSafeEqual to prevent timing attacks
  if (generatedHash.length !== providedHmac.length || !crypto.timingSafeEqual(generatedHash, providedHmac)) {
    return res.status(400).send('HMAC validation failed');
  }
  // --- End HMAC Validation ---

  if (shop && code) {
    // Exchange the authorization code for an access token
    const accessTokenRequestUrl = `https://${shop}/admin/oauth/access_token`;
    const accessTokenPayload = {
      client_id: SHOPIFY_API_KEY,
      client_secret: SHOPIFY_API_SECRET,
      code,
    };

    try {
      const response = await axios.post(accessTokenRequestUrl, accessTokenPayload);
      const accessToken = response.data.access_token;

      // Store the access token for future API calls
      shops[shop] = accessToken; 

      console.log(`✅ Access token for ${shop} successfully obtained and stored: ${accessToken}`);

      // Successful installation redirect or response
      // In a real app, you would redirect to the app's UI inside the Shopify admin.
      res.status(200).send('App installed successfully! Access token retrieved.');

    } catch (error) {
      console.error('Error exchanging code for access token:', error.response ? error.response.data : error.message);
      res.status(500).send('Error obtaining access token.');
    }
  } else {
    res.status(400).send('Required parameters missing in callback.');
  }
});


app.listen(port, () => {
  console.log(`🚀 Example app listening at http://localhost:${port}`);
});
Enter fullscreen mode Exit fullscreen mode

Running and Installing the App

1. Start Your Local Server

Open a terminal in your project directory:

node index.js
Enter fullscreen mode Exit fullscreen mode

2. Start ngrok

Open a separate terminal window and expose your local server (port 3000) to the internet:

ngrok http 3000
Enter fullscreen mode Exit fullscreen mode

ngrok will provide a public HTTPS URL (e.g., https://random-string.ngrok.io). Keep both terminals running.

3. Update Shopify App Settings

Go back to your app settings in the Shopify Partner Dashboard:

  • Update the App URL to your ngrok HTTPS URL (e.g., https://random-string.ngrok.io).
  • Update the Allowed redirection URL(s) to your ngrok URL with the callback path (e.g., https://random-string.ngrok.io/shopify/callback).
  • Save the changes.

4. Install the App

Open your browser and navigate to the installation route, replacing the placeholders with your ngrok URL and development store URL:

[https://random-string.ngrok.io/shopify?shop=your-development-shop.myshopify.com](https://random-string.ngrok.io/shopify?shop=your-development-shop.myshopify.com)
Enter fullscreen mode Exit fullscreen mode

You will be redirected to the Shopify installation screen. Click Install app. If successful, your browser will display "App installed successfully! Access token retrieved." and the token will be printed in your Node.js console.


Making an Authenticated API Call

Once you have the access token, you can use it to make secure requests to the Shopify Admin API.

Add this new route to your index.js file before app.listen():

// ... (add this inside your index.js)

// Endpoint to fetch products from the installed shop
app.get('/products', async (req, res) => {
  const shop = req.query.shop;
  const accessToken = shops[shop];

  if (!accessToken) {
    return res.status(403).send('Access token not found for this shop. Please install the app first.');
  }

  // Use the API version from your environment variables
  const shopifyApiUrl = `https://${shop}/admin/api/${SHOPIFY_API_VERSION}/products.json`;

  try {
    const response = await axios.get(shopifyApiUrl, {
      headers: {
        'X-Shopify-Access-Token': accessToken, // The token is passed as a header
      },
    });
    // Send the product data back to the client
    res.status(200).json(response.data); 
  } catch (error) {
    console.error('Shopify API Error:', error.response ? error.response.data : error.message);
    res.status(500).send('Error fetching products from Shopify API.');
  }
});
Enter fullscreen mode Exit fullscreen mode

To test this, restart your Node.js server (node index.js) and visit the new endpoint:

[https://random-string.ngrok.io/products?shop=your-development-shop.myshopify.com](https://random-string.ngrok.io/products?shop=your-development-shop.myshopify.com)
Enter fullscreen mode Exit fullscreen mode

You should see a JSON output containing the products from your development store!


Next Steps

This guide gives you the basic authentication mechanism. From here, you can:

  • Implement a database to store shop data and access tokens permanently.
  • Build a user interface (frontend) for your app.
  • Explore Webhooks to subscribe to events (like new orders).
  • Use the official Shopify Node API Library for a more robust development experience.

Top comments (0)