Building a Shopify application with Node.js and Express

In this tutorial you'll build a Node.js + Express application that connects to Shopify. Your application will authenticate with a shop, request a permanent access token, and then use that access token to make an API call.

Requirements

To follow this tutorial, you need the following system requirements:

Step 1: Expose your local development environment to the internet

As part of the authentication process, Shopify redirects the user from the app authorization prompt back to your application. This requires that your application have a public HTTPS address (localhost:3000 is not a public address). A simple solution is to use a service like ngrok to create a secure tunnel from the public internet to your local machine.

To create a public HTTPS address with ngrok:

  1. Download ngrok.
  2. Start an ngrok tunnel on port 3000:

    # if ngrok is in your downloads folder:
    $ cd ~/Downloads/
    # start an HTTP tunnel on port 3000
    $ ./ngrok http 3000
  3. Copy the HTTPS forwarding address that's generated, for example, https://81c14560.ngrok.io. You'll enter it in Shopify as your app URL when you create your app.

Step 2: Create and configure your app in the Partner Dashboard

To configure a new app in your Partner Dashboard:

  1. Create an app from your Partner Dashboard.
  2. When you're creating the app, enter the app URL in the following format:

    {https ngrok forwarding address}/shopify
  3. After creating the app, click App info.

  4. Under Whitelisted redirection URL(s), enter your app's callback URL in the following format:

     {https ngrok forwarding address}/shopify/callback
  5. In the App credentials section, take note of your API key and and API secret key. You'll use these as environment variables in your app.

When your app's published to the Shopify App Store, install requests are sent to your app URL with a shop parameter (for example, https://your-app.com/shopify?shop=your-development-shop.myshopify.com). Your app will use that parameter to generate an install URL that redirects the merchant to the app authorization prompt. This process is explained later in the tutorial.

Step 3: Create a Node.js project

To create a Node.js project:

  1. In a new console tab, create a folder named shopify-express-application, and then run npm init from within the folder to create a package.json file:

    $ mkdir shopify-express-application
    $ cd shopify-express-application
    $ npm init
  2. When prompted, enter the application name as shopify-express-application, and then press return to accept the default for each following option. When you've accepted all the defaults, you'll see text printed to the console that looks something like this:

    {
      "name": "shopify-express-application",
      "version": "1.0.0",
      "description": "",
      "main": "index.js",
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
      },
      "author": "",
      "license": "ISC"
    }

    Your project now includes a package.json file, and your project's structure should resemble this:

    |- shopify-express-application
      |- package.json
  3. Use npm install to install required packages to your application:

    $ npm install express dotenv cookie nonce request request-promise --save

    The --save command tells npm to add the packages as dependencies in the package.json:

    {
      "name": "shopify-express-application",
      "version": "1.0.0",
      "description": "",
      "main": "index.js",
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
      },
      "author": "",
      "license": "ISC",
      "dependencies": {
        "cookie": "^0.3.1",
        "dotenv": "^4.0.0",
        "express": "^4.15.4",
        "nonce": "^1.0.4",
        "request": "^2.82.0",
        "request-promise": "^4.2.1"
      }
    }
  4. Create a file called .env, and then enter your Shopify API key and API secret key in the following format:

    SHOPIFY_API_KEY="{YOUR_API_KEY}"
    SHOPIFY_API_SECRET="{YOUR_API_SECRET_KEY}"
  5. Create a .gitignore file with the following content:

    .env

    This makes sure that your credentials are never saved to GitHub or any other Git provider that you use.

After creating your project, your project structure should look like this:

|- shopify-express-application
  |- node_modules
  |- package.json
  |- .env
  |- .gitignore

Step 4: Start building your Node.js app

To start building your Node.js app:

  1. Create an index.js file, and then add the following example code to get your app started with a Hello World! route:

    const dotenv = require('dotenv').config();
    const express = require('express');
    const app = express();
    const crypto = require('crypto');
    const cookie = require('cookie');
    const nonce = require('nonce')();
    const querystring = require('querystring');
    const request = require('request-promise');
    
    const apiKey = process.env.SHOPIFY_API_KEY;
    const apiSecret = process.env.SHOPIFY_API_SECRET;
    const scopes = 'read_products';
    const forwardingAddress = "{ngrok forwarding address}"; // Replace this with your HTTPS Forwarding address
    
    app.get('/', (req, res) => {
      res.send('Hello World!');
    });
    
    app.listen(3000, () => {
      console.log('Example app listening on port 3000!');
    });
  2. Replace {ngrok forwarding address} with the ngrok forwarding address that you generated.

  3. Run your app with the following node command:

    $ node index.js
  4. Visit localhost:3000 from your browser to see the Hello World! route.

Step 5: Create Shopify-specific routes

You need to add two Shopify-specific routes to your app:

The install route

The install route expects a shop URL parameter, which it uses to redirect the merchant to the Shopify app authorization prompt where they can choose to accept or reject the installation request.

Add the following code for the install route to your index.js file:

app.get('/shopify', (req, res) => {
  const shop = req.query.shop;
  if (shop) {
    const state = nonce();
    const redirectUri = forwardingAddress + '/shopify/callback';
    const installUrl = 'https://' + shop +
      '/admin/oauth/authorize?client_id=' + apiKey +
      '&scope=' + scopes +
      '&state=' + state +
      '&redirect_uri=' + redirectUri;

    res.cookie('state', state);
    res.redirect(installUrl);
  } else {
    return res.status(400).send('Missing shop parameter. Please add ?shop=your-development-shop.myshopify.com to your request');
  }
});

The URL that redirects the merchant to the app authorization prompt needs to include the app's API key, the scopes that the app is requesting to access, a state parameter that will be used in the callback route to make sure that the request originated from the app, and the redirect_uri where the merchant will be sent after Shopify approves the request. In constructing this URL, you've used the forwardingAddress, apiKey, and scopes variables declared in the previous step.

After adding the code to your index.js file, restart your app, and then visit {your ngrok forwarding address}/shopify?shop=your-development-shop.myshopify.com. You should be redirected to the app authorization prompt for your development shop.

The callback route

After a user accepts the install request, Shopify sends them to the redirect_uri that you specified in the previous step. This address needs match the URL that you entered under Whitelisted redirection URL(s) in the Partner Dashboard. The request from Shopify includes a code parameter that needs to be exchanged for a permanent access token.

  1. Add the following code to your index.js file:

    app.get('/shopify/callback', (req, res) => {
      const { shop, hmac, code, state } = req.query;
      const stateCookie = cookie.parse(req.headers.cookie).state;
    
      if (state !== stateCookie) {
        return res.status(403).send('Request origin cannot be verified');
      }
    
      if (shop && hmac && code) {
        res.status(200).send('Callback route');
    
        // TODO
        // Validate request is from Shopify
        // Exchange temporary code for a permanent access token
          // Use access token to make API call to 'shop' endpoint
      } else {
        res.status(400).send('Required parameters missing');
      }
    });
  2. Your app needs to validate the request by using HMAC validation to make sure that the request has come from Shopify. To validate the request, replace res.status(200).send('Callback route'); with the following code:

    const map = Object.assign({}, req.query);
    delete map['signature'];
    delete map['hmac'];
    const message = querystring.stringify(map);
    const providedHmac = Buffer.from(hmac, 'utf-8');
    const generatedHash = Buffer.from(
      crypto
        .createHmac('sha256', apiSecret)
        .update(message)
        .digest('hex'),
        'utf-8'
      );
    let hashEquals = false;
    // timingSafeEqual will prevent any timing attacks. Arguments must be buffers
    try {
      hashEquals = crypto.timingSafeEqual(generatedHash, providedHmac)
    // timingSafeEqual will return an error if the input buffers are not the same length.
    } catch (e) {
      hashEquals = false;
    };
    
    if (!hashEquals) {
      return res.status(400).send('HMAC validation failed');
    }
    
    res.status(200).send('HMAC validated');

    With the validation code included, your route will now look like this:

    app.get('/shopify/callback', (req, res) => {
      const { shop, hmac, code, state } = req.query;
      const stateCookie = cookie.parse(req.headers.cookie).state;
    
      if (state !== stateCookie) {
        return res.status(403).send('Request origin cannot be verified');
      }
    
      if (shop && hmac && code) {
        // DONE: Validate request is from Shopify
        const map = Object.assign({}, req.query);
        delete map['signature'];
        delete map['hmac'];
        const message = querystring.stringify(map);
        const providedHmac = Buffer.from(hmac, 'utf-8');
        const generatedHash = Buffer.from(
          crypto
            .createHmac('sha256', apiSecret)
            .update(message)
            .digest('hex'),
            'utf-8'
          );
        let hashEquals = false;
    
        try {
          hashEquals = crypto.timingSafeEqual(generatedHash, providedHmac)
        } catch (e) {
          hashEquals = false;
        };
    
        if (!hashEquals) {
          return res.status(400).send('HMAC validation failed');
        }
    
        res.status(200).send('HMAC validated');
        // TODO
        // Exchange temporary code for a permanent access token
          // Use access token to make API call to 'shop' endpoint
      } else {
        res.status(400).send('Required parameters missing');
      }
    });
  3. Restart the app, and then visit {your ngrok address}/shopify?shop=your-development-shop.myshopify.com from your browser. If you previously accepted the install request, then Shopify won't present you with the prompt again. Your browser should now load a page that reads HMAC validated.

  4. To exchange the provided code parameter for a permanent access_token, replace res.status(200).send('HMAC validated'); with the following code:

    const accessTokenRequestUrl = 'https://' + shop + '/admin/oauth/access_token';
    const accessTokenPayload = {
      client_id: apiKey,
      client_secret: apiSecret,
      code,
    };
    
    request.post(accessTokenRequestUrl, { json: accessTokenPayload })
    .then((accessTokenResponse) => {
      const accessToken = accessTokenResponse.access_token;
    
      res.status(200).send("Got an access token, let's do something with it");
      // TODO
      // Use access token to make API call to 'shop' endpoint
    })
    .catch((error) => {
      res.status(error.statusCode).send(error.error.error_description);
    });

    Your route will now look like this:

    app.get('/shopify/callback', (req, res) => {
      const { shop, hmac, code, state } = req.query;
      const stateCookie = cookie.parse(req.headers.cookie).state;
    
      if (state !== stateCookie) {
        return res.status(403).send('Request origin cannot be verified');
      }
    
      if (shop && hmac && code) {
        // DONE: Validate request is from Shopify
        const map = Object.assign({}, req.query);
        delete map['signature'];
        delete map['hmac'];
        const message = querystring.stringify(map);
        const providedHmac = Buffer.from(hmac, 'utf-8');
        const generatedHash = Buffer.from(
          crypto
            .createHmac('sha256', apiSecret)
            .update(message)
            .digest('hex'),
            'utf-8'
          );
        let hashEquals = false;
    
        try {
          hashEquals = crypto.timingSafeEqual(generatedHash, providedHmac)
        } catch (e) {
          hashEquals = false;
        };
    
        if (!hashEquals) {
          return res.status(400).send('HMAC validation failed');
        }
    
        // DONE: Exchange temporary code for a permanent access token
        const accessTokenRequestUrl = 'https://' + shop + '/admin/oauth/access_token';
        const accessTokenPayload = {
          client_id: apiKey,
          client_secret: apiSecret,
          code,
        };
    
        request.post(accessTokenRequestUrl, { json: accessTokenPayload })
        .then((accessTokenResponse) => {
          const accessToken = accessTokenResponse.access_token;
    
          res.status(200).send("Got an access token, let's do something with it");
          // TODO
          // Use access token to make API call to 'shop' endpoint
        })
        .catch((error) => {
          res.status(error.statusCode).send(error.error.error_description);
        });
    
      } else {
        res.status(400).send('Required parameters missing');
      }
    });
  5. Restart the app, and then visit {your ngrok address}/shopify?shop=your-development-shop.myshopify.com. The app should bring you to a page that reads Got an access token, let's do something with it.

  6. To use the access token to make an API call to the shop endpoint, replace res.status(200).send("Got an access token, let's do something with it"); with the following code:

    const shopRequestUrl = 'https://' + shop + '/admin/shop.json';
    const shopRequestHeaders = {
      'X-Shopify-Access-Token': accessToken,
    };
    
    request.get(shopRequestUrl, { headers: shopRequestHeaders })
    .then((shopResponse) => {
      res.end(shopResponse);
    })
    .catch((error) => {
      res.status(error.statusCode).send(error.error.error_description);
    });

Your completed callback route should now look like this:

app.get('/shopify/callback', (req, res) => {
  const { shop, hmac, code, state } = req.query;
  const stateCookie = cookie.parse(req.headers.cookie).state;

  if (state !== stateCookie) {
    return res.status(403).send('Request origin cannot be verified');
  }

  if (shop && hmac && code) {
    // DONE: Validate request is from Shopify
    const map = Object.assign({}, req.query);
    delete map['signature'];
    delete map['hmac'];
    const message = querystring.stringify(map);
    const providedHmac = Buffer.from(hmac, 'utf-8');
    const generatedHash = Buffer.from(
      crypto
        .createHmac('sha256', apiSecret)
        .update(message)
        .digest('hex'),
        'utf-8'
      );
    let hashEquals = false;

    try {
      hashEquals = crypto.timingSafeEqual(generatedHash, providedHmac)
    } catch (e) {
      hashEquals = false;
    };

    if (!hashEquals) {
      return res.status(400).send('HMAC validation failed');
    }

    // DONE: Exchange temporary code for a permanent access token
    const accessTokenRequestUrl = 'https://' + shop + '/admin/oauth/access_token';
    const accessTokenPayload = {
      client_id: apiKey,
      client_secret: apiSecret,
      code,
    };

    request.post(accessTokenRequestUrl, { json: accessTokenPayload })
    .then((accessTokenResponse) => {
      const accessToken = accessTokenResponse.access_token;
      // DONE: Use access token to make API call to 'shop' endpoint
      const shopRequestUrl = 'https://' + shop + '/admin/shop.json';
      const shopRequestHeaders = {
        'X-Shopify-Access-Token': accessToken,
      };

      request.get(shopRequestUrl, { headers: shopRequestHeaders })
      .then((shopResponse) => {
        res.status(200).end(shopResponse);
      })
      .catch((error) => {
        res.status(error.statusCode).send(error.error.error_description);
      });
    })
    .catch((error) => {
      res.status(error.statusCode).send(error.error.error_description);
    });

  } else {
    res.status(400).send('Required parameters missing');
  }
});

Step 6: Run your app

With your routes completed, restart your app, and then visit {your ngrok forwarding address}/shopify?shop=your-development-shop.myshopify.com. This will bring you to a page that displays the raw JSON from your request to the shop endpoint.

Next steps

  1. Explore Node.js libraries such as Shopify Node API, and Shopify API Node.
  2. Use the Embedded App SDK to allow merchants to load your app from within the Shopify admin.

Sign up for a Partner account to get started.

Sign up