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

Douds_Man
Tourist
5 0 4

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;

 

 

Replies 6 (6)
sagar_at_avyya
Shopify Partner
16 1 5

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.

 

Sagar @ Avyya - Modern Sales booster
https://apps.shopify.com/avyya
sagar_at_avyya
Shopify Partner
16 1 5

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',);
  }
}

 

 

Sagar @ Avyya - Modern Sales booster
https://apps.shopify.com/avyya
Sajeel
Shopify Partner
272 31 25

hy bro i am facing this issue when i try to call function from server.js

Response {type: 'basic', url: 'https://21a7-182-176-104-93.ngrok.io/auth?shop=undefined', redirected: true, status: 200, ok: true, …}

 

Sajeel
Shopify Partner
272 31 25

server.js function code

//=====================script tags====================//
router.get("/scripttag", verifyRequest(), async(ctx,resp) => {
const { shop, accessToken } = ctx.session;
const url = `https://${shop}/admin/api/2022-01/script_tags.json`;
const src='https://example.com/example.js';

const shopifyHeader = (token)=>({
"Content-Type": "application/json",
"X-Shopify-Access-Token":token,
});
const scriptTagBody = JSON.stringify({
script_tag: {
event: 'onload',
src,
}
});
await axios.post(url,scriptTagBody,{headers:shopifyHeader(accessToken)});
});
//=====================end script tags====================//

Danh11
Shopify Partner
24 0 2

I don't understand how this is reaching the endpoint. Your example of props.axios_instance.get("/products"works, but replacing 'products' with 'collections' doesn't work (the response just contains HTML code). Nor does 'themes', which is what I'm actually trying to do.

 

Ultimately, I want to be able to make a request for a list of themes, to then grab a theme ID which will let me grab an asset. The app has all the right scopes and authentication.

nyrsimon
New Member
1 0 0

Thanks Sagar this helped a lot!