Shopify node backend- frontend communication - shopify

I am really new in shopify app development.
I have an allready a working app what i have created with next.JS (I have worked with node/express too)
I just would like to create a connection between my frontend and backend with a simple endpoint.
It means i send a get request and i receive something nonsense. The main goal would be that is the backend can communicate with the frontend.
I have created a git repo too.: https://github.com/akospaska/shopify-outofthebox
The app has been created with shopify-cli
In my pages folder there is an index.js file, where my frontend "lives". 
I have created (or i think ) 2 differend endpoints.
pages/api/test   endpoint: "/test"
server/server.js  endpoint: "/test2"
When i call the endpoints i get an error. 
I have read the documentation but it just makes me confused.
How should i authenticate between my backend and frontend exactly?
Thank you for your help Guys in advance.

The endpoints aren't pages, they are routes on your express app.
Here is a related question with answer:
Node backend communication between react frontend and node backend | Shopify related
Here is a checklist for you how to set up an endpoint (POST):
1.) Navigate to your index.js file in the /web directory
2.) Insert this code:
app.post("/api/test", async (req, res) => {
try {
res.status(201).send(response);
} catch (error) {
res.status(500).send(error.message);
}
});
}
app.post() sets up a route in your project.
3.) Navigate to your index.jsx file in /pages directory and insert this code (I set up a callback when a form submit button is clicked):
const handleSubmit = useCallback(
(body) => {
(async () => {
const parsedBody = body;
const response = await fetch("/api/test?shop=YOUR_SHOP_URL, {
method: "POST",
body: parsedBody
});
if (response.ok) {
console.log("Success");
}
})();
return { status: "success" };
},
[]
);
<Form onSubmit={handleSubmit}>
</Form>
it should call this API endpoint. So now you communicate with an API endpoint.
Maybe I could help you with my answer!

Related

404 error calling GET API from a shopify theme-extension

I'm new to shopify development, and can't figure out how to call an authenticated API from a shopify theme-extension. Essentially, I'm trying to make a theme extension, where one of the functionalities is that when a checkbox is clicked, an API that counts the number of products is called.
I have a working api that gets the product count, and in web>index.js, I have set-up the end-point:
app.get("/api/products/count", async (_req, res) => {
const countData = await shopify.api.rest.Product.count({
session: res.locals.shopify.session,
});
res.status(200).send(countData);
});
Under web>frontend>hooks, I have the authenticated hooks set-up as shown below. I've tested that if I call the "api/products/count" API from one of the web pages using useAppQuery, it works as expected, and returns the product count.
import { useAuthenticatedFetch } from "./useAuthenticatedFetch";
import { useMemo } from "react";
import { useQuery } from "react-query";
export const useAppQuery = ({ url, fetchInit = {}, reactQueryOptions }) => {
const authenticatedFetch = useAuthenticatedFetch();
const fetch = useMemo(() => {
return async () => {
const response = await authenticatedFetch(url, fetchInit);
return response.json();
};
}, [url, JSON.stringify(fetchInit)]);
return useQuery(url, fetch, {
...reactQueryOptions,
refetchOnWindowFocus: false,
});
};
In my theme extension code, I've added an event listener to the checkbox which calls getProductCount. In getProductCount, I want to call /api/products/count:
import { useAppQuery } from "../../../web/frontend/hooks";
export const getProductCount = (product) => {
const {
data,
refetch: refetchProductCount,
isLoading: isLoadingCount,
isRefetching: isRefetchingCount,
} = useAppQuery({
url: "/api/products/count",
reactQueryOptions: {
onSuccess: () => {
setIsLoading(false);
},
},
});
}
However, when I run locally and click the checkbox, it returns a 404 error trying to find useAppQuery. The request URL is https://cdn.shopify.com/web/frontend/hooks. It seems like the authentication isn't working because that URL looks incorrect.
Am I missing a step that I need to do in order to call an authenticated API from a theme-extension?
I thought the issue was just the import path for useAppQuery but I've tried different paths, and they all return the same 404 issue.
If you want a hot tip here. In your theme App extension, you do not actually need to make an API call to get a product count. In your theme app extension, you can just use Liquid, and dump the product count out to a variable of your choice, and use the count, display the count, do whatever.
{{ shop.product_count }}
Of course, this does not help you if you need other storefront API calls in your theme App extension, but whatever. In my experience, I render the API Access Token I need in my theme app extension, and then making my Storefront API calls is just a fetch().
The only time I would use authenticated fetch, is when I am doing embedded App API calls, but that is a different beast from a theme app extension. In there, you do not get to make authenticated calls as the front-end is verboten for those of course. Instead you'd use App Proxy for security.
TL:DR; Storefront API calls with a token should not fail with a 404 if you call the right endpoint. You can use Storefront API inside a theme app extension. Inside a theme app extension, if you need backend Admin API access, you can use App Proxy calls.

How do I mock server-side API calls in a Nextjs app?

I'm trying to figure out how to mock calls to the auth0 authentication backend when testing a next js app with React Testing Library. I'm using auth0/nextjs-auth0 to handle authentication. My intention is to use MSW to provide mocks for all API calls.
I followed this example in the nextjs docs next.js/examples/with-msw to set up mocks for both client and server API calls. All API calls generated by the auth0/nextjs-auth0 package ( /api/auth/login , /api/auth/callback , /api/auth/logout and /api/auth/me) received mock responses.
A mock response for /api/auth/me is shown below
import { rest } from 'msw';
export const handlers = [
// /api/auth/me
rest.get(/.*\/api\/auth\/me$/, (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
user: { name: 'test', email: 'email#domain.com' },
}),
);
}),
];
The example setup works fine when I run the app in my browser. But when I run my test the mocks are not getting picked up.
An example test block looks like this
import React from 'react';
import {render , screen } from '#testing-library/react';
import Home from 'pages/index';
import App from 'pages/_app';
describe('Home', () => {
it('should render the loading screen', async () => {
render(<App Component={Home} />);
const loader = screen.getByTestId('loading-screen');
expect(loader).toBeInTheDocument();
});
});
I render the page inside the App component like this <App Component={Home} /> so that I will have access to the various contexts wrapping the pages.
I have spent about 2 days on this trying out various configurations and I still don't know what I might be doing wrong. Any and every help is appreciated.
This is probably resolved already for the author, but since I ran into the same issue and could not find useful documentation, this is how I solved it for end to end tests:
Overriding/configuring the API host.
The plan is to have the test runner start next.js as custom server and then having it respond to both the next.js, as API routes.
A requirements for this to work is to be able to specify the backend (host) the API is calling (via environment variables). Howerver, access to environment variables in Next.js is limited, I made this work using the publicRuntimeConfig setting in next.config.mjs. Within that file you can use runtime environment variables which then bind to the publicRuntimeConfig section of the configuration object.
/** #type {import('next').NextConfig} */
const nextConfig = {
(...)
publicRuntimeConfig: {
API_BASE_URL: process.env.API_BASE_URL,
API_BASE_PATH: process.env.API_BASE_PATH,
},
(...)
};
export default nextConfig;
Everywhere I reference the API, I use the publicRuntimeConfig to obtain these values, which gives me control over what exactly the (backend) is calling.
Allowing to control the hostname of the API at runtime allows me to change it to the local machines host and then intercept, and respond to the call with a fixture.
Configuring Playwright as the test runner.
My e2e test stack is based on Playwright, which has a playwright.config.ts file:
import type { PlaywrightTestConfig } from '#playwright/test';
const config: PlaywrightTestConfig = {
globalSetup: './playwright.setup.js',
testMatch: /.*\.e2e\.ts/,
};
export default config;
This calls another file playwright.setup.js which configures the actual tests and backend API mocks:
import {createServer} from 'http';
import {parse} from 'url';
import next from 'next';
import EndpointFixture from "./fixtures/endpoint.json";
// Config
const dev = process.env.NODE_ENV !== 'production';
const baseUrl = process?.env?.API_BASE_URL || 'localhost:3000';
// Context
const hostname = String(baseUrl.split(/:(?=\d)/)[0]).replace(/.+:\/\//, '');
const port = baseUrl.split(/:(?=\d)/)[1];
const app = next({dev, hostname, port});
const handle = app.getRequestHandler();
// Setup
export default async function playwrightSetup() {
const server = await createServer(async (request, response) => {
// Mock for a specific endpoint, responds with a fixture.
if(request.url.includes(`path/to/api/endpoint/${EndpointFixture[0].slug}`)) {
response.write(JSON.stringify(EndpointFixture[0]));
response.end();
return;
}
// Fallback for pai, notifies about missing mock.
else if(request.url.includes('path/to/api/')) {
console.log('(Backend) mock not implementeded', request.url);
return;
}
// Regular Next.js behaviour.
const parsedUrl = parse(request.url, true);
await handle(request, response, parsedUrl);
});
// Start listening on the configured port.
server.listen(port, (error) => {
console.error(error);
});
// Inject the hostname and port into the applications publicRuntimeConfig.
process.env.API_BASE_URL = `http://${hostname}:${port}`;
await app.prepare();
}
Using this kind of setup, the test runner should start a server which responds to both the routes defined by/in Next.js as well as the routes intentionally mocked (for the backend) allowing you to specify a fixture to respond with.
Final notes
Using the publicRuntimeConfig in combination with a custom Next.js servers allows you to have a relatively large amount of control about the calls that are being made on de backend, however, it does not necessarily intercept calls from the frontend, the existing frontend mocks might stil be necessary.

How to use a private API key with Nuxt (on the client)?

Problem Solved
If you're struggling with the same issue, look at the accepted answer which is one way to achieve it by using serverMiddleware
I'm using an API which required a private key. I've stored the key inside a .env file, and called it in the nuxt configuration file, like this :
privateRuntimeConfig: {
secretKey: process.env.MY_SECRET_KEY
},
My API call is done inside the asyncData() hook on my index page. It works fine when i load this page, or reload it, but everytime i use the navigation to come back to this page, i end up with an error (I use a buffer to convert my API key to base64)
First argument must be a string, Buffer, ArrayBuffer, Array, or array-like object.
After some research and debugging, i found out that my private key wasn't available at the time, and the "secret" value used in my api call was "undefined".
The thing I don't get is why is this working on initial load / reload but not on page navigation ? And is there a way to fix it without using a backend ? (SSR for SEO and the ability to use private keys without exposing them are the main reasons why i used Nuxt for my project)
Here is my code :
async asyncData({ $content, store, $config }) {
const secret = Buffer.from($config.secretKey).toString('base64')
const request = await fetch('https://app.snipcart.com/api/products', {
headers: {
'Authorization': `Basic ${secret}`,
'Accept': 'application/json'
}
})
const result = await request.json()
store.commit('products/addProducts', result)
const stocks = store.getters['products/getProducts']
return { stocks }
},
Update
Looking at the #nuxtjs/snipcart module's key key and since it's a buildModules, you can totally put it there since it will be available only during the build (on Node.js only)!
For more info, Snipcart do have a lot of blog posts, this one based on Nuxt may help clearing things up: https://www.storyblok.com/tp/how-to-build-a-shop-with-nuxt-storyblok-and-snipcart
You do have your key initially because you're reaching the server when you enter the page or hard refresh it.
If you navigate after the hydration, it will be a client side navigation so you will not be able to have access to the private key. At the end, if your key is really private (nowadays, some API provide keys that can be exposed), you'll need to work around it in some ways.
Looking at Snipcart: https://docs.snipcart.com/v3/api-reference/authentication, it clearly states that the key should be available in
Appear in your compiled front-end assets (HTML, JavaScript)
Meanwhile, if you need to make another call to your backend (trying to access something else than products), you'll need to make a second call.
With Nuxt2, you cannot reach for the backend each time as of right now since you will stay in an SPA context (Nuxt is a server then client Vue app basically). But you could write down the token into a cookie or even better, use a backend as a proxy to hide this specific key (or even a serverless function).
Some more info can be found on my other answer here: https://stackoverflow.com/a/69575243/8816585
Thanks #kissu for your (very) quick answer :)
So, based on what you said and your other answer on the subject, i've made a server Middleware in Nuxt in my server folder.
server/snipcart.js
const bodyParser = require('body-parser')
const axios = require('axios')
const app = require('express')()
app.use(bodyParser.json())
app.all('/getProducts', (request, response) => {
const url = 'https://app.snipcart.com/api/products'
const secret = Buffer.from(process.env.SNIPCART_SECRET).toString('base64')
const config = {
headers: {
'Authorization': `Basic ${secret}`,
'Accept': 'application/json'
}
}
axios
.get(url, config)
.then(res => {
const products = {}
res.data.items.forEach(
item => {
const productId = item.userDefinedId.replace(/-/g, '')
const stocks = {}
item.variants.forEach(
variant => {
const size = variant.variation[0].option
const stock = variant.stock
stocks[size] = stock
}
)
products[productId] = stocks
}
)
response.json(products)
})
.catch( err => response.json(err) )
})
module.exports = app
Correct me if i'm wrong, but I think that's basically the same as using a server as a proxy right ? Based on Nuxt lifecycle hooks, the serverMiddleware one is only run on the server, so my API key shouldn't be exposed to the client ? (I still need to do some refactoring to clean the code, but at least it's working) (https://nuxtjs.org/docs/concepts/nuxt-lifecycle/#server & https://nuxtjs.org/docs/configuration-glossary/configuration-servermiddleware/)
nuxt.config.js
serverMiddleware: [
{ path: "/server", handler: "~/server/snipcart.js" }
]
index.vue (where my snipcart API call was previously made, i guess now I should move this call directly from the product card component where the data is needed) :
async asyncData({ $content, store, $axios }) {
await $axios
.get('/server/getProducts')
.then(res => store.commit('products/addProducts', res.data))
.catch(err => console.log(err))
const stocks = store.getters['products/getProducts']
return {stocks, masterplanProducts }
},
PS : Snipcart does provide a public API key, but the use is very limited. In order to access the remaining stock for each product, i have to use the private key (which allows for some other operations, like removing products / accessing orders and such)
UPDATE :
It's not working when the website is fists accessed from any other page than the one one where the API call is, since the store won't have any data from the API call)
Okay, now I feel dumb. I found a way to make it work. I guess taking the time to explain my problem helped me understand how to solve it.
For those who encounter a similar issue, i fixed it by wrapping my API call with a If statement.
if ($config.secretKey) {
const secret = Buffer.from($config.secretKey).toString('base64')
const request = await fetch('https://app.snipcart.com/api/products', {
headers: {
'Authorization': `Basic ${secret}`,
'Accept': 'application/json'
}
})
const result = await request.json()
store.commit('products/addProducts', result)
}
const stocks = store.getters['products/getProducts']
This way, i can just skip the API call and access values from my vuex store.

Nuxtjs Redis cache implementation for API calls inside components

I am using the plugin Nuxt Perfect Cache to server-side cache my QPI requests to an external service.
I am using the cacheFetch method on Component level and this component is loaded on a dynamic page (defined by its slug). When I navigate to the dynamic page, the API call is not cached in Redis, however when I reload the page, the caching happens as expected.
Below is how my code is structured:
_slug.js (for /users)
<template>
<h1>{{ user.name }}</h1>
<Posts :author = user.id>
</template>
<script>
import Posts from '~/components/Posts.vue'
export default {
components: { Posts },
async asyncData({params}) {
const user = await fetch(`/users/${params.slug}`)
.then(res => res.json())
}
}
</script>
And inside Posts.vue I use the perfect cache cacheFetch method to fetch the list of posts, something like:
props: ['author'],
async fetch() {
this.posts = await this.$cacheFetch({ key:`user--#{this.author}--posts`, expire: 60 * 60 },
async () => {
return await fetch(`/users/#{this.author}/posts`).then(res => res.json())
})
},
data() {
return {
posts: []
}
}
When I load the user page directly in the browser, the json response for the posts is saved in Redis as expected. When I navigate from within the application using a NuxtLink, the user page is displayed correctly (including the posts), but no key is set or get from Redis.
How can I ensure the API calls are cached when users interact with the app?
redis is only available in server side not client side when you are navigating in client side you don't have access to redis you can set absolute link to render server side when user is navigating but I don't recommend this.
the best solution is cache data in redis in your api.

Next.js & Ant Design Dragger: File upload fails on deployed instance

I'm trying to build a file upload with Next.js and Ant Design using React.
On localhost, everything works fine. When I deployed the instance and try to upload a file, I get the following error:
Request URL: https://my-app.my-team.now.sh/url/for/test/
Request Method: POST
Status Code: 405
Remote Address: 34.65.228.161:443
Referrer Policy: no-referrer-when-downgrade
The UI that I use looks like the following:
<Dragger {...fileUploadProps}>{renderImageUploadText()}</Dragger>
where fileUploadProps are:
const fileUploadProps = {
name: 'file',
multiple: false,
showUploadList: false,
accept: 'image/png,image/gif,image/jpeg',
onChange(info) {
const { status } = info.file;
if (status === 'done') {
if (info.file.size > 2000000) {
setUploadSizeError('File size is too large');
} else {
handleFieldValue(API_FORM_FIELDS.PICTURE, info);
}
} else if (status === 'error') {
setUploadSizeError(`${info.file.name} file upload failed.`);
}
},
};
I assume, it has to do with the server side rendering of Next.js? On the other hand, it might not, because by the time I navigated to url/for/test it should render on the client.
How do you implemented file uploads with Ant Design and Next.js?
Got this to work by passing the prop action="https://www.mocky.io/v2/5cc8019d300000980a055e76" to the <Upload/> component.
The ANTD Upload component must make a POST request to upload the file. It either makes that POST request to the current url, which results in the 405, or to the url specified by the action prop. https://www.mocky.io/v2/5cc8019d300000980a055e76 works as this url.
Inspired by Trey's answer (and because I didn't want to send data anywhere outside my domain) I made a no-op api route that simply returns a success, and then pointed Ant Design <Upload>'s action to /api/noop
/* pages/api/noop.tsx */
import { NextApiRequest, NextApiResponse } from 'next'
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
res.status(200).end('noop')
}
/* Wherever I'm using <Upload /> */
<Upload
...otherProps
action={'/api/noop'}
>
Note: this is specific to Next.js, which makes it easy to create API routes.
This works for me.
By default, the ANTD upload component makes a POST request to upload the file. So, to avoid this, add a customRequest props on Upload as below.
customRequest={({ onSuccess }) => setTimeout(() => { onSuccess("ok", null); }, 0) }