How do you connect to a custom route in server.js from a React frontend?

Solved
saricden
Tourist
4 0 0

Hi all,

I'm attempting to get a simple test route in my server/server.js connected to a component in my pages/ directory.

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: ApiVersion.October20,
  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}`);
    }
  });

  // THIS IS THE ROUTE I WANT TO CONNECT TO
  router.get(
    "/init_fauna",
    // verifyRequest({ returnHeader: true }),
    async (ctx, next) => {
      ctx.status = 201;
      ctx.body = "Woohoo";
    }
  );

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

  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}`);
  });
});

 

index.js (Index component):

import React, { useState } from 'react';
import { Page, Layout, EmptyState} from "@shopify/polaris";
import { ResourcePicker, TitleBar, useAppBridge, getSessionToken } from '@shopify/app-bridge-react';
import store from 'store-js';
import ResourceListWithProducts from './components/ResourceList';

const img = 'https://cdn.shopify.com/s/files/1/0757/9955/files/empty-state.svg';

export const Index = () => {
  const app = useAppBridge();
  const [open, setOpen] = useState(false);
  const emptyState = !store.get('ids');

  const handleSelection = (resources) => {
    const idsFromResources = resources.selection.map((product) => product.id);
    setOpen(false);
    store.set('ids', idsFromResources);
  };

  const testFauna = async () => {
    const res = await fetch("/api/fauna_init");

    const json = await res.json();

    console.log(json);
  };


  return (
    <Page>
      <TitleBar
        primaryAction={{
          content: 'Select products',
          onAction: () => setOpen(true),
        }}
        secondaryActions={[
          {
            content: 'Test Fauna',
            onAction: testFauna
          }
        ]}
      />
      <ResourcePicker
        resourceType="Product"
        showVariants={false}
        open={open}
        onSelection={(resources) => handleSelection(resources)}
        onCancel={() => setOpen(false)}
      />
      {emptyState ? ( // Controls the layout of your app's empty state
        <Layout>
          <EmptyState
            heading="Discount your products temporarily"
            action={{
              content: 'Select products',
              onAction: () => setOpen(true),
            }}
            image={img}
          >
            <p>Select products to change their price temporarily.</p>
          </EmptyState>
        </Layout>
      ) : (
        // Uses the new resource list that retrieves products by IDs
        <ResourceListWithProducts />
      )}
    </Page>
  );
}

export default Index;

 

However when I look at what's happening in Chrome's network inspector I see this:

Screenshot from 2021-10-18 18-02-42.png

Note how it's attempting to reach auth?shop=undefined.

I have followed the basic tutorials trying to get things setup (October, 2021 versions), but I haven't came across anything that talks about connecting your frontend to custom backend routes, which I'm assuming you need to do to interface with an external database, such as in my case FaunaDB.

If someone could point me in the right direction I would appreciate it immensely, I've been at this for hours and have made little progress.

Accepted Solution (1)

Accepted Solutions
olavoasantos
Shopify Staff
8 2 6

This is an accepted solution.

Hey @saricden. How are you?

My name is Olavo and I'm a developer from Shopify. Thank you for reaching out!

I was able to reproduce your issue here and it seems that the catch-all route is handling these requests. Because it doesn't have the `shop` query param, it redirects to the `/auth?shop=undefined` route.

As @tewe suggested, the simplest solution is to declare your route as `/api/init_fauna`. That will allow you to make the fetch request. Alternatively, to get this working with authentication, I'd suggest using Next.js' API routes and passing along the session token with the request.

Let me walk you through how to do that:

1. Create a new file `pages/api/init_fauna.js` containing:

 

export default function handler(req, res) {
  res.status(201).json('Woohoo')
}

 


2. On the `server/server.js` file, update the catch-all route to look for the shop on the query param and on the session:

 

router.get("(.*)", async (ctx) => {
  const shop =
    ctx.query.shop ||
    (await Shopify.Utils.loadCurrentSession(ctx.req, ctx.res))?.shop; // This retrieves the shop from the session

  // 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);
  }
});

 

 
3. On your `pages/index.js` file, you should pass the session token with the request. You can do this by leveraging the `authenticatedFetch` utility:

 

// ... more imports
import { authenticatedFetch } from "@shopify/app-bridge-utils";
import { ResourcePicker, TitleBar, useAppBridge } from '@shopify/app-bridge-react';

export const Index = () => {
  const app = useAppBridge();
  // ... more logic

  const testFauna = async () => {
    const authFetch = authenticatedFetch(app);
    const res = await authFetch("/api/init_fauna");

    const json = await res.json();

    console.log(json);
  };

  return (/* Your JSX */);
}

 


I hope this helps. Please let me know if you have any more questions.

View solution in original post

Replies 7 (7)
tewe
Trailblazer
211 39 90

Hi @saricden ,

if I see it correctly you have to name the route in the server.js `

/api/fauna_init

Then it should work.

Regards

Thomas

• Was my reply helpful? Click Like to let me know!
• Was your question answered? Mark it as an Accepted Solution
• Check out our Price Updater App
olavoasantos
Shopify Staff
8 2 6

This is an accepted solution.

Hey @saricden. How are you?

My name is Olavo and I'm a developer from Shopify. Thank you for reaching out!

I was able to reproduce your issue here and it seems that the catch-all route is handling these requests. Because it doesn't have the `shop` query param, it redirects to the `/auth?shop=undefined` route.

As @tewe suggested, the simplest solution is to declare your route as `/api/init_fauna`. That will allow you to make the fetch request. Alternatively, to get this working with authentication, I'd suggest using Next.js' API routes and passing along the session token with the request.

Let me walk you through how to do that:

1. Create a new file `pages/api/init_fauna.js` containing:

 

export default function handler(req, res) {
  res.status(201).json('Woohoo')
}

 


2. On the `server/server.js` file, update the catch-all route to look for the shop on the query param and on the session:

 

router.get("(.*)", async (ctx) => {
  const shop =
    ctx.query.shop ||
    (await Shopify.Utils.loadCurrentSession(ctx.req, ctx.res))?.shop; // This retrieves the shop from the session

  // 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);
  }
});

 

 
3. On your `pages/index.js` file, you should pass the session token with the request. You can do this by leveraging the `authenticatedFetch` utility:

 

// ... more imports
import { authenticatedFetch } from "@shopify/app-bridge-utils";
import { ResourcePicker, TitleBar, useAppBridge } from '@shopify/app-bridge-react';

export const Index = () => {
  const app = useAppBridge();
  // ... more logic

  const testFauna = async () => {
    const authFetch = authenticatedFetch(app);
    const res = await authFetch("/api/init_fauna");

    const json = await res.json();

    console.log(json);
  };

  return (/* Your JSX */);
}

 


I hope this helps. Please let me know if you have any more questions.

View solution in original post

saricden
Tourist
4 0 0

Fantastic thank you @olavoasantos that worked for me.

Just a followup question, is it safe to store private keys in ENV variables and pass them to Next API routes in this fashion?

(I'm looking to communicate with a database).

Thanks!

olavoasantos
Shopify Staff
8 2 6

Dealing with env variables and secrets is always delicate. So it's always good to be extra careful. Next.js has good control over env variables and forces you to explicitly opt into making env variables public (take a look here).

So, to answer your question. Yes, it's ok to use env variables. But remember never to commit `.env` files containing your secrets. Going down this path, it's usually a good practice to commit a `.env.example` which contains the keys with no values. Then, on your production server, you rename the`.env.example` to `.env` and fill in the secrets.

I hope this helped

saricden
Tourist
4 0 0

One followup question @olavoasantos - do you know how I can send the authenticated shop data to my API route?

I'm just looking to associate the store name or ID with records in a database now.

Thanks!

olavoasantos
Shopify Staff
8 2 6

Hey @saricden.

Going back to my original response, I suggested that you use the `authenticatedFetch`. What this will do is attach the current session token as a header to your request (if you check the network request, it should contain an `authorization` header with a `Bearer {TOKEN}`). On your API, you can then use the `Shopify.Utils.loadCurrentSession` function to decode that token and get information about the shop.

Breaking it down, you'll do:

1. On your `pages/index.js` file (or in whatever component you'll do this), you should pass the session token with the request. You can do this by leveraging the `authenticatedFetch` utility:

// ... more imports
import { authenticatedFetch } from "@shopify/app-bridge-utils";
import { ResourcePicker, TitleBar, useAppBridge } from '@shopify/app-bridge-react';

export const Index = () => {
  const app = useAppBridge();
  // ... more logic

  const testFauna = async () => {
    const authFetch = authenticatedFetch(app);
    const res = await authFetch("/api/init_fauna");

    const json = await res.json();

    console.log(json);
  };

  return (/* Your JSX */);
}

 

2. On your `pages/api/init_fauna.js` (or in whatever handler you'll need this), you can decode the session token using the `loadCurrentSession` utility:

import Shopify from "@shopify/shopify-api";

export default async function handler(req, res) {
  const session = await Shopify.Utils.loadCurrentSession(req, res);

  // ... do your magic ...

  res.status(201).json(('Woohoo'))
}

 

Looking at your `session` object, it should have the following shape:

interface Session {
    id: string;
    shop: string;
    state: string;
    scope: string;
    expires?: Date;
    isOnline?: boolean;
    accessToken?: string;
    onlineAccessInfo?: 
      expires_in: number;
      associated_user_scope: string;
      associated_user: {
          id: number;
          first_name: string;
          last_name: string;
          email: string;
          email_verified: boolean;
          account_owner: boolean;
          locale: string;
          collaborator: boolean;
      };
  };
}

 

You should be able to get what you need from this. Also make sure the `session` object is not `undefined`. If there's no session, you can throw an exception and/or try to re-authenticate the user.

saricden
Tourist
4 0 0