Node.js/React Generated App with Rest API Call Example

Douds_Man
Tourist
4 0 2

Hello Everyone,

I am a new developer and recently decided to build myself a dummy application that uses the Shopify Admin API. I created this app by using the Shopify CLI. At first, I assumed that this would be an extremely easy task since I have used APIs in the past but I very quickly realized that this was not the case. I came to these forums and read all of the documentation and was just completely lost on accomplishing this simple task. As I went through all of the forums and documentation I began to grow angry that there was not a posted "All in one solution" to a simple REST API call. After many hours, I finally was able to successfully request a list of products from the Admin API. I really hope this can help a lot of you out and prevent many headaches. Here is my code... 

server.js

 

import "@babel/polyfill";
import dotenv from "dotenv";
import "isomorphic-fetch";
import createShopifyAuth, { verifyRequest } from "@shopify/koa-shopify-auth";
import Shopify, { ApiVersion } from "@shopify/shopify-api";
import Koa from "koa";
import next from "next";
import Router from "koa-router";


dotenv.config();
const port = parseInt(process.env.PORT, 10) || 8081;
const dev = process.env.NODE_ENV !== "production";
const app = next({
  dev,
});
const handle = app.getRequestHandler();

Shopify.Context.initialize({
  API_KEY: process.env.SHOPIFY_API_KEY,
  API_SECRET_KEY: process.env.SHOPIFY_API_SECRET,
  SCOPES: process.env.SCOPES.split(","),
  HOST_NAME: process.env.HOST.replace(/https:\/\//, ""),
  API_VERSION: "2021-07",
  IS_EMBEDDED_APP: true,
  // This should be replaced with your preferred storage strategy
  SESSION_STORAGE: new Shopify.Session.MemorySessionStorage(),
});

// Storing the currently active shops in memory will force them to re-login when your server restarts. You should
// persist this object in your app.
const ACTIVE_SHOPIFY_SHOPS = {};
app.prepare().then(async () => {
  const server = new Koa();
  const router = new Router();
  server.keys = [Shopify.Context.API_SECRET_KEY];
  
  server.use(
    createShopifyAuth({
      async afterAuth(ctx) {


        // Access token and shop available in ctx.state.shopify
        const { shop, accessToken, scope } = ctx.state.shopify;
        const host = ctx.query.host;
        ACTIVE_SHOPIFY_SHOPS[shop] = scope;

        const response = await Shopify.Webhooks.Registry.register({
          shop,
          accessToken,
          path: "/webhooks",
          topic: "APP_UNINSTALLED",
          webhookHandler: async (topic, shop, body) =>
            delete ACTIVE_SHOPIFY_SHOPS[shop],
        });

        if (!response.success) {
          console.log(
            `Failed to register APP_UNINSTALLED webhook: ${response.result}`
          );
        }

        // Redirect to app with shop parameter upon auth
        ctx.redirect(`/?shop=${shop}&host=${host}`);
      },
    })
  );

  const handleRequest = async (ctx) => {
    await handle(ctx.req, ctx.res);
    ctx.respond = false;
    ctx.res.statusCode = 200;
  };



  router.post("/webhooks", async (ctx) => {
    try {
      await Shopify.Webhooks.Registry.process(ctx.req, ctx.res);
      console.log(`Webhook processed, returned status code 200`);
    } catch (error) {
      console.log(`Failed to process webhook: ${error}`);
    }
  });

  router.post(
    "/graphql",
    verifyRequest({ returnHeader: true }),
    async (ctx, next) => {
      await Shopify.Utils.graphqlProxy(ctx.req, ctx.res);
    }
  );



  router.get("/products", async (ctx) => {
    const session = await Shopify.Utils.loadCurrentSession(ctx.req, ctx.res);
    // Create a new client for the specified shop.
    const client = new Shopify.Clients.Rest(session.shop, session.accessToken);
    // Use `client.get` to request the specified Shopify REST API endpoint, in this case `products`.
    const products = await client.get({
      path: 'products',
    });

    ctx.body = products;
    ctx.status = 200;
  });



  router.get("(/_next/static/.*)", handleRequest); // Static content is clear
  router.get("/_next/webpack-hmr", handleRequest); // Webpack content is clear



  router.get("(.*)", async (ctx) => {
    const shop = ctx.query.shop;

    // This shop hasn't been seen yet, go through OAuth to create a session
    if (ACTIVE_SHOPIFY_SHOPS[shop] === undefined) {
      ctx.redirect(`/auth?shop=${shop}`);
    } else {
      await handleRequest(ctx);
    }
  });


  server.use(router.allowedMethods());
  server.use(router.routes());
  server.listen(port, () => {
    console.log(`> Ready on http://localhost:${port}`);
  });
});

 



 _app.js

 

import ApolloClient from "apollo-boost";
import { ApolloProvider } from "react-apollo";
import App from "next/app";
import { AppProvider } from "@shopify/polaris";
import { Provider, useAppBridge } from "@shopify/app-bridge-react";
import { authenticatedFetch, getSessionToken } from "@shopify/app-bridge-utils";
import { Redirect } from "@shopify/app-bridge/actions";
import "@shopify/polaris/dist/styles.css";
import translations from "@shopify/polaris/locales/en.json";
import axios from 'axios';

function userLoggedInFetch(app) {
  const fetchFunction = authenticatedFetch(app);

  return async (uri, options) => {
    const response = await fetchFunction(uri, options);

    if (
      response.headers.get("X-Shopify-API-Request-Failure-Reauthorize") === "1"
    ) {
      const authUrlHeader = response.headers.get(
        "X-Shopify-API-Request-Failure-Reauthorize-Url"
      );

      const redirect = Redirect.create(app);
      redirect.dispatch(Redirect.Action.APP, authUrlHeader || `/auth`);
      return null;
    }

    return response;
  };
}

function MyProvider(props) {
  const app = useAppBridge();

  const client = new ApolloClient({
    fetch: userLoggedInFetch(app),
    fetchOptions: {
      credentials: "include",
    },
  });


  const axios_instance = axios.create();
  // Intercept all requests on this Axios instance
  axios_instance.interceptors.request.use(function (config) {
  return getSessionToken(app) // requires a Shopify App Bridge instance
    .then((token) => {
      // Append your request headers with an authenticated token
      config.headers["Authorization"] = `Bearer ${token}`;
      return config;
    });
});

  const Component = props.Component;

  return (
    <ApolloProvider client={client}>
      <Component {...props} axios_instance={axios_instance}/>
    </ApolloProvider>
  );
}

class MyApp extends App {
  render() {
    const { Component, pageProps, host } = this.props;
    return (
      <AppProvider i18n={translations}>
        <Provider
          config={{
            apiKey: API_KEY,
            host: host,
            forceRedirect: true,
          }}
        >
          <MyProvider Component={Component} {...pageProps} />
        </Provider>
      </AppProvider>
    );
  }
}

MyApp.getInitialProps = async ({ ctx }) => {
  return {
    host: ctx.query.host,
  };
};

export default MyApp;

 




index.js

 

import { Heading, Page, TextField } from "@shopify/polaris";


function Index(props){
      async function getProducts(){
        const res = await props.axios_instance.get("/products");
        return res;
      }

      async function handleClick() {
        const result = await getProducts();
        console.log(result);
      }

    return (
      <Page>
        <Heading>Shopify app with Node and React </Heading>
        <input
          value="Update Pages"
          type="submit"
          onClick={handleClick}
        ></input>
      </Page>
    );
}

export default Index;

 

 

sagar_ranglani
Tourist
6 0 1

Great Post!

There is one more version of this: https://community.shopify.com/c/shopify-apis-and-sdks/example-rest-call-in-new-node-library/td-p/114...

where fetchFunctions is sent through props. That one seems simpler, but we would have to use fetch instead of axios.

The only (and important) edition in this code would be to somehow put the custom fetch / axios method in global scope using contexts or HOC. Passing the fetch method to a deeply nested component would create a big hassle otherwise.

 

0 Likes
sagar_ranglani
Tourist
6 0 1

This is the best solution I found for this which allows appBridge inside a hook:

https://community.shopify.com/c/shopify-apis-and-sdks/getting-session-token-in-axios-intercept/m-p/1...

Here is my version of it, which works:

import axios from "axios";
import { useAppBridge } from '@shopify/app-bridge-react';
import { getSessionToken } from '@shopify/app-bridge-utils';

const config = {
	headers: {
		'Content-Type': 'application/json',
		Accept: 'application/json',
	},
};
/*
this is custom hook that fetch data from my backend
*/
export const useBackendApiClient = () => {
	const app = useAppBridge();
  const instance = axios.create();
  instance.interceptors.request.use(function (config) {

    return getSessionToken(app) // requires a Shopify App Bridge instance
      .then((token) => {
        // Append your request headers with an authenticated token
        config.headers["Authorization"] = `Bearer ${token}`;
        return config;
      });
  });
	return instance;
};

 

Usage in a component:

import { useBackendApiClient } from "../../hooks/useBackendApi";

const MyComponent = (props) => {
  const BackEndApiClient = useBackendApiClient();

  const handleSave = async () => {
    const res = await BackEndApiClient.get('/xxx',);
  }
}

 

 

0 Likes