We've extended the functionality of this library so that it can now run on JavaScript runtimes other than Node.js, as long as there is a runtime adapter for it.
To migrate your Node.js app to use this new adaptable version of the API library, you'll need to add an import of the node adapter in your app, before importing the library functions themselves. Note you only need to import this once in your app.
import '@shopify/shopify-api/adapters/node';
import { ... } from '@shopify/shopify-api';
This automatically sets the library up to run on the Node.js runtime.
In addition to making the library compatible with multiple runtimes, we've also improved its public interface to make it easier for apps to load only the features they need from the library. Once you set up your app with the right adapter, you can follow the next sections for instructions on how to upgrade the individual methods that were changed.
Note 1: the examples below are assuming your app is running using Express.js, but the library still works with different frameworks as before.
Note 2: in general, method calls in v6 of the library take parameter objects instead of positional parameters. A
To make it easier to navigate this guide, here is an overview of the sections it contains:
- Updating
package.json
to use new version - Renamed
Shopify.Context
toshopify.config
- Passing in framework requests / responses
- Simplified namespace for errors
- Changes to package exports
- Changes to authentication functions
- Changes to
Session
andSessionStorage
- Changes to API clients
- Billing
- Utility functions
- Changes to webhook functions
- Changes to use of REST resources
- Example migration
To use the v6
version of the library, update the apps package.json
file to refer to the new version
"dependencies": {
- "@shopify/shopify-api": "^5.0.0",
+ "@shopify/shopify-api": "^6.0.0",
After the file is updated, install the updated library with your preferred package manager.
yarn install
# or
npm install
# or
pnpm install
We've refactored the way objects are exported by this library, to remove the main static singleton Shopify
object with global settings stored in Shopify.Context
.
Even though that object has no business logic, the fact that the configuration is global made it hard to mock for tests and to set up multiple instances of it. Part of the changes we made were to create a library object with local settings, to make it feel more idiomatic and easier to work with.
The settings that were previously set in Shopify.Context
are now returned in the config
property in the api instance return by shopifyApi()
.
In general, those changes don't affect any library functionality, unless explicitly mentioned below. You will probably need to search and replace most of the imports to this library to leverage the new approach, but it should not require any functionality changes.
-
Apps no longer set up the library with
Shopify.Context.initialize()
. The new library object constructor takes in the configuration.Beforeimport {Shopify} from '@shopify/shopify-api'; Shopify.Context.initialize({ API_KEY: '...', ... });
Afterimport {shopifyApi} from '@shopify/shopify-api'; const shopify = shopifyApi({ apiKey: '...', ... });
-
Shopify.Context
was replaced withshopify.config
, and config options are nowcamelCase
instead ofUPPER_CASE
.Beforeconsole.log(Shopify.Context.API_KEY);
Afterconsole.log(shopify.config.apiKey);
-
Shopify.Context.throwIfUnitialized
andUninitializedContextError
were removed.
Using the v5 or earlier version of the library, apps would generally call library functions as follows:
app.get(
'/path',
async (req: http.IncomingMessage, res: http.ServerResponse) => {
const redirectUri = await Shopify.Auth.beginAuth(req, res);
res.redirect(redirectUri);
},
);
To enable the library to work across runtimes, we've abstracted these objects into rawRequest
and rawResponse?
.
As a general rule, you'll need to update calls like above to:
app.get(
'/path',
async (req: http.IncomingMessage, res: http.ServerResponse) => {
// Library will automatically trigger the redirect in res
await shopify.auth.begin({rawRequest: req, rawResponse: res});
},
);
For runtimes that expect handlers to return a Response
object, rather than passing it in as a parameter (like CloudFlare workers), you can skip the rawResponse
value.
async function handleFetch(request: Request): Promise<Response> {
// Library will return the Response object
return shopify.auth.begin({rawRequest: request});
},
Using the v5 or earlier version of the library, error types were available via the Shopify
instance, e.g., Shopify.Errors.HttpResponseError
. In v6, they are now exported by the library and can be imported without any namespace prefix.
app.post('/webhooks', async (req, res) => {
try {
await Shopify.Webhooks.Registry.process(req, res);
} catch (error) {
if (error instanceof Shopify.Errors.InvalidWebhookError) {
console.log(`Webhook processing error:\n\tmessage = ${error.message}`);
} else {
console.log('Other error:\n\t', error);
}
}
});
import {InvalidWebhookError} from '@shopify/shopify-api';
// ...
app.post('/webhooks', async (req, res) => {
try {
await shopify.webhooks.process({
rawBody: (req as any).rawBody, // as a string
rawRequest: req,
rawResponse: res,
});
} catch (error) {
if (error instanceof InvalidWebhookError) {
console.log(
`Webhook processing error:\n\tmessage = ${error.message}\n\tresponse = ${error.response}`,
);
} else {
console.log('Other error:\n\t', error);
}
}
});
In v5 and earlier versions, certain exports needed to be imported from deep paths into the package. In v6, only some export paths are allowed and are specific to various needs of the application. For example, for the list of GDPR webhook topics,
import {gdprTopics} from '@shopify/shopify-api/dist/webhooks/registry.js';
import {gdprTopics} from '@shopify/shopify-api';
See the Changes to use of REST resources section on how to access REST resources in v6.
The OAuth methods still behave the same way, but we've updated their signatures to make it easier to work with them. See the updated OAuth instructions for a complete example.
See Access modes for more details regarding how to use the isOnline
parameter.
-
Shopify.Auth.beginAuth()
is nowshopify.auth.begin()
, it takes in an object, and it now also triggers a redirect response to the correct endpoint.Beforeconst redirectUri = await Shopify.Auth.beginAuth( req, res, 'my-shop.com', '/auth/callback', true, ); // App had to redirect to the returned URL res.redirect(redirectUri);
⚠️ After// Library handles redirecting await shopify.auth.begin({ shop: 'my-shop.com', callbackPath: '/auth/callback', isOnline: true, rawRequest: req, rawResponse: res, });
-
Shopify.Auth.validateAuthCallback()
is nowshopify.auth.callback()
, it now takes an object with the request and response, but thequery
argument is no longer necessary - it will be read from the request. This method will set the appropriate response headers.Beforeconst session = Shopify.Auth.validateAuthCallback( req, res, req.query as AuthQuery, ); // session.accessToken... res.redirect('url');
⚠️ Afterconst callbackResponse = shopify.auth.callback({ rawRequest: req, rawResponse: res, }); // callbackResponse.session.accessToken... res.redirect('url');
-
The
shopify.auth
component only exports the key functions now to make the API simpler, sogetCookieSessionId
,getJwtSessionId
,getOfflineSessionId
,getCurrentSessionId
are no longer exported. They're internal library functions. -
There is a new
shopify.session
object which contains session-specific functions. See the Utility functions section for the specific changes.
-
The
SessionStorage
interface has been removed and any provided implementions have been removed from the library. The library only provides methods to obtain sessionId's. Responsibility for storing sessions is delegated to the application.Note The previous implementations of session storage have been converted into their own standalone packages in the
Shopify/shopify-app-js
repo (see the list in the Implementing session storage guide). -
The
Session
constructor now takes an object which allows all properties of a session, andSession.cloneSession
was removed since we can use a session as arguments for the clone.Beforeimport {Session} from '@shopify/shopify-api'; const session = new Session( 'session-id', 'shop.myshopify.com', 'state1234', true, ); session.accessToken = 'token'; const clone = Session.cloneSession(session, 'newId');
⚠️ Afterimport {Session} from '@shopify/shopify-api'; const session = new Session({ id: 'session-id', shop: 'shop.myshopify.com', state: 'state1234', isOnline: true, accessToken: 'token', }); const clone = new Session({...session, id: 'newId'});
-
The
isActive()
method ofSession
now takes ascopes
parameter. If the scopes of the session don't match the scopes of the application (e.g., app has been restarted with new scopes), the session is deemed to be inactive and OAuth should be initiated again.Beforeconst session = await Shopify.Utils.loadCurrentSession(req, res); if (!session.isActive()) { // current session is not active - either expired or scopes have changed }
⚠️ Afterconst sessionId = await shopify.session.getCurrentId({ isOnline: true, rawRequest: req, rawResponse: res, }); // use sessionId to retrieve session from app's session storage // getSessionFromStorage() must be provided by application const session = await getSessionFromStorage(sessionId); if (!session.isActive(shopify.config.scopes)) { // current session is not active - either expired or scopes have changed }
-
The
Session
class now includes a.toObject
method to support the app in storingSession
objects. The return value of.toObject
can be passed tonew Session()
to create aSession
object.const callbackResponse = shopify.auth.callback({ rawRequest: req, rawResponse: res, }); // app stores Session in its own storage mechanism await addSessionToStorage(callbackResponse.session.toObject());
-
See the Implementing session storage guide for the changes you'll need to make to load and store your sessions. In general, you'll need to store sessions that are returned from
shopify.auth.callback()
and you'll need to load sessions anywhere your code previously usedloadCurrentSession
.
The constructor for each API client that this package provides now takes an object of arguments, rather than positional ones. The returned objects behave the same as they did previously.
-
REST Admin API client:
Beforeconst restClient = new Shopify.Clients.Rest( session.shop, session.accessToken, );
⚠️ Afterconst restClient = new shopify.clients.Rest({session});
-
GraphQL Admin API client:
Beforeconst graphqlClient = new Shopify.Clients.Graphql( session.shop, session.accessToken, );
⚠️ Afterconst graphqlClient = new shopify.clients.Graphql({session});
-
Storefront API client:
Beforeconst storefrontClient = new Shopify.Clients.Storefront( session.shop, storefrontAccessToken, );
⚠️ Afterconst storefrontClient = new shopify.clients.Storefront({ domain: session.shop, storefrontAccessToken, });
The HttpResponseError
, HttpRetriableError
, HttpInternalError
, and HttpThrottlingError
classes were updated to include more information on the errors.
catch (err) {
if (err instanceof HttpResponseError) {
console.log(err.code, err.statusText);
}
}
catch (err) {
if (err instanceof HttpResponseError) {
console.log(err.response.code, err.response.statusText, err.response);
}
}
The billing functionality hasn't changed, but the main difference is that the library now provides separate methods for checking and requesting payment, which gives apps more freedom to implement their billing logic.
To configure billing, you can now pass in more than one billing plan, and they're indexed by plan name:
Shopify.Context.initialize({
// ...
billing: {
chargeName: 'My plan',
amount: 5.0,
currencyCode: 'USD',
interval: BillingInterval.Every30Days,
},
});
const shopify = shopifyApi({
// ...
billing: {
'My plan': {
amount: 5.0,
currencyCode: 'USD',
interval: BillingInterval.Every30Days,
},
},
});
We broke the Shopify.Billing.check
method up into shopify.billing.check
and shopify.billing.request
, as mentioned above:
const {hasPayment, confirmBillingUrl} = await Shopify.Billing.check({
session,
isTest: true,
});
if (!hasPayment) {
return redirect(confirmBillingUrl);
}
const hasPayment = await shopify.billing.check({
session,
plans: 'My plan',
isTest: true,
});
if (!hasPayment) {
const confirmationUrl = await shopify.billing.request({
session,
plan: 'My plan',
isTest: true,
});
return redirect(confirmationUrl);
}
Note that when calling check
, apps can pass in one or more plans, and it will return true if any of them match. The request
method creates a charge using the configuration of the given plan name.
The previous Shopify.Utils
object contained functions that relied on the global configuration object, but those have been refactored to use the instance-specific configuration.
We also felt that the Utils
object had some functions that belong to other parts of the library, so some of these functions have been moved to other sub-objects - keep an eye out for those changes in the list below!
Here are all the specific changes that we made to the Utils
object:
-
Shopify.Utils.generateLocalHmac
was removed because it's only meant to be used internally by the library. -
The
storeSession
method was removed since sessions are no longer stored by the library. Apps are now fully responsible for implementating session storage and can save data to their sessions as they please. See the implementing session storage guide for the changes you'll need to make to store your sessions. -
validateHmac
is nowasync
.Beforeconst isValid = Shopify.Utils.validateHmac(req.query);
Afterconst isValid = await shopify.utils.validateHmac(req.query);
-
nonce
,safeCompare
, andgetEmbeddedAppUrl
have moved toshopify.auth
.getEmbeddedAppUrl
is nowasync
and takes in an object.Beforeconst nonce = Shopify.Utils.nonce(); const match = Shopify.Utils.safeCompare(strA, strB); const redirectUrl = Shopify.Utils.getEmbeddedAppUrl(req, res);
⚠️ Afterconst nonce = shopify.auth.nonce(); const match = shopify.auth.safeCompare(strA, strB); const redirectUrl = await shopify.auth.getEmbeddedAppUrl({ rawRequest: req, rawResponse: res, });
-
Shopify.Utils.decodeSessionToken
is nowshopify.session.decodeSessionToken
, and it'sasync
.Beforeconst payload = Shopify.Utils.decodeSessionToken(token);
Afterconst payload = await shopify.session.decodeSessionToken(token);
-
Shopify.Utils.loadCurrentSession
is nowshopify.session.getCurrentId
, it takes in an object, theisOnline
param is mandatory, and it now returns a session id, that can then be used to retrieve the session details from app-provided storage.Beforeconst session = await Shopify.Utils.loadCurrentSession(req, res);
⚠️ Afterconst sessionId = await shopify.session.getCurrentId({ isOnline: true, rawRequest: req, rawResponse: res, });
-
Shopify.Utils.deleteCurrentSession
has been removed, as the library no longer handles the storage of sessions. -
Shopify.Utils.loadOfflineSession
is nowshopify.session.getOfflineId
, and takes ashop
argument as its only parameter. It still does not validate the given arguments and should only be used if you trust the source of the shop argument. It returns a session id that can then be used to retrieve the session details from app-provided storage.Beforeconst session = await Shopify.Utils.loadOfflineSession( 'my-shop.myshopify.com', true, );
⚠️ Afterconst sessionId = await shopify.session.getOfflineId( 'my-shop.myshopify.com', );
-
Shopify.Utils.deleteOfflineSession
has been removed, as the library no longer handles the storage of sessions. -
Shopify.Utils.withSession
has been removed, as the library no longer handles the storage of sessions. The various clients have been updated to take a session as an argument.Beforeconst {client, session} = await Shopify.Utils.withSession({ clientType: 'rest', isOnline: true, req, res, shop: 'my-shop.myshopify.com', });
⚠️ Afterconst sessionId = await shopify.session.getCurrentId({ isOnline: true, rawRequest: req, rawResponse: res, }); // use sessionId to retrieve session from app's session storage // getSessionFromStorage() must be provided by application const session = await getSessionFromStorage(sessionId); const gqlClient = await shopify.clients.Graphql({session}); // or const restClient = await shopify.clients.Rest({session}); // or const storefrontClient = await shopify.clients.Storefront({ session, storefrontAccessToken, });
-
Shopify.Utils.graphqlProxy
is nowshopify.clients.graphqlProxy
, it takes a session argument, and it also takes the body as an argument instead of parsing it from the request. This will make it easier for apps to use a body parser with this function.Beforeconst response = await Shopify.Utils.graphqlProxy(req, res);
⚠️ Afterconst sessionId = await shopify.session.getCurrentId({ isOnline: true, rawRequest: req, rawResponse: res, }); // use sessionId to retrieve session from app's session storage // getSessionFromStorage() must be provided by application const session = await getSessionFromStorage(sessionId); const response = await shopify.clients.graphqlProxy({ session, rawBody: req.rawBody, // From my app });
Shopify.Context.LOG_FILE
was replaced with shopify.config.logger.log
so it can work without file-system access.
Shopify.Context.initialize({LOG_FILE: 'path/to/file.log'});
const shopify = new shopifyApi({
logger: {
log: async (_severity: string, message: string) => {
fs.appendFile('path/to/file.log', message);
},
},
});
We've also added more logging to this package to make it easier for apps to debug issues and track what's happening.
See the documentation for logger
settings.
The package formats the message in a consistent way before calling the log
function.
In v6, apps can now set up multiple handlers per webhook topic, and we simplified the library's interface to reduce the number of methods needed.
The following methods are now available:
addHandlers
register
process
getHandlers
getTopicsAdded
replaces the previousgetTopics
And the following methods are no longer supported:
addHandler
registerAll
getHandler
isWebhookPath
For more details on the updated webhook functions, see the documentation.
Below are instructions on how webhooks work now:
- You can call
shopify.webhooks.addHandlers
with a list of handlers, indexed by topic. Note that:
-
you must now use delivery method-specific fields instead of
path
, likecallbackUrl
andarn
, to match the GrahpQL API behavior -
this is now
async
, so you'll need toawait
itBeforeShopify.Webhooks.Registry.addHandler('PRODUCTS_CREATE', { path: '/webhooks', webhookHandler: handleWebhookRequest, });
⚠️ Afterawait shopify.webhooks.addHandlers({ PRODUCTS_CREATE: { deliveryMethod: DeliveryMethod.Http, callbackUrl: '/webhooks', callback: handleWebhookRequest, }, TOPIC_1: [handler, handler2], TOPIC_2: handler3, });
Note: if you're using TypeScript, the handlers are typed based on the
deliveryMethod
you select.
-
Shopify.Webhooks.Registry.register
is nowshopify.webhooks.register
, and it now only takes in the session. It will register any handlers you added, and delete registrations that don't match the new configuration. That means the final webhooks registered for a shop will always match your configuration.Beforeconst response = await Shopify.Webhooks.Registry.register({ path: '/webhooks', topic: 'PRODUCTS_CREATE', accessToken: session.accessToken, shop: session.shop, });
⚠️ Afterconst response = await shopify.webhooks.register({session: session}); // Response will be a list indexed by topic: console.log(response[topic][0].success, response[topic][0].result);
-
The response from
register
has a different structure in v6 when compared to v5 or earlier.Beforeresponse = { TOPIC: { success: boolean; result: any; } }, TOPIC2: { // ... }, }
⚠️ Afterresponse = { TOPIC: [ { deliveryMethod: DeliveryMethod; success: boolean; result: any; }, { deliveryMethod: DeliveryMethod; success: boolean; result: any; }, ], TOPIC2: [ // ... ], } // DeliveryMethod is an enum whose value can be one of // - DeliveryMethod.Http // - DeliveryMethod.EventBridge // - DeliveryMethod.PubSub
As with earlier versions, when
success
is nottrue
,result
should contain anerrors
property with an array of errormessage
's, e.g.,response = { PRODUCTS_CREATE: [ { deliveryMethod: DeliveryMethod.Http; success: false; result: { errors: [ { message: "Error message", } ], }; }, ], } if (!response["PRODUCTS_CREATE"][0].success) { console.log(response["PRODUCTS_CREATE"][0].result.errors[0].message); // "Error message" }
-
Shopify.Webhooks.Registry.process
is nowshopify.webhooks.process
, and it takes the body as an argument instead of parsing it from the request. This will make it easier for apps to use a body parser with this function.Beforeapp.post('/webhooks', async (req, res) => { try { await Shopify.Webhooks.Registry.process(req, res); } catch (error) { console.log(error.message); } });
⚠️ Afterapp.post('/webhooks', async (req, res) => { try { // Note: this example assumes that the raw content of the body of the request // has been read and is available at req.rawBody; this will likely differ // depending on which body parser is used. await shopify.webhooks.process({ rawBody: (req as any).rawBody, // as a string rawRequest: req, rawResponse: res, }); } catch (error) { console.log(error.message); } });
-
Shopify.Webhooks.Registry.getHandler
is nowshopify.webhooks.getHandlers
, and returns all handler configurations for that topic.BeforeShopify.Webhooks.Registry.addHandler({ topic: 'PRODUCTS', path: '/webhooks', webhookHandler: productsWebhookHandler, }); const productsHandler = Shopify.Webhooks.Registry.getHandler('PRODUCTS'); // productsHandler = {path: '/webhooks', webhookHandler: productsWebhookHandler}
⚠️ Afterawait shopify.webhooks.addHandlers({ PRODUCTS: { deliveryMethod: DeliveryMethod.Http, callbackUrl: '/webhooks', callback: productsWebhookHandler, }, }); // Is an array of handler configurations const handlers = shopify.webhooks.getHandlers('PRODUCTS');
-
Shopify.Webhooks.Registry.getTopics
is nowshopify.webhooks.getTopicsAdded
, to indicate that it returns the topics added usingaddHandlers
.BeforeShopify.Webhooks.Registry.addHandlers({ PRODUCTS_CREATE: { path: '/webhooks', webhookHandler: productCreateWebhookHandler, }, PRODUCTS_DELETE: { path: '/webhooks', webhookHandler: productDeleteWebhookHandler, }, }); const topics = Shopify.Webhooks.Registry.getTopics(); // topics = ['PRODUCTS_CREATE', 'PRODUCTS_DELETE']
⚠️ Afterawait shopify.webhooks.addHandlers({ PRODUCTS_CREATE: { deliveryMethod: DeliveryMethod.Http, callbackUrl: '/webhooks', callback: productCreateWebhookHandler, }, PRODUCTS_DELETE: { deliveryMethod: DeliveryMethod.Http, callbackUrl: '/webhooks', callback: productDeleteWebhookHandler, }, }); const topics = shopify.webhooks.getTopicsAdded(); // topics = ['PRODUCTS_CREATE', 'PRODUCTS_DELETE']
REST resources were added to version 3 of the API library and were accessed by importing directly from the dist
folder of the @shopify/shopify-api
.
Starting with v6, they can now be accessed via the Shopify API instance directly.
app.get('/api/products/count', async (req, res) => {
const session = await Shopify.Utils.loadCurrentSession(req, res, false);
const {Product} = await import(
`@shopify/shopify-api/dist/rest-resources/${Shopify.Context.API_VERSION}/index.js`
);
const countData = await Product.count({session});
res.status(200).send(countData);
});
import {shopifyApi, ApiVersion} from '@shopify/shopify-api';
const apiVersion = ApiVersion.October22;
let {restResources} = await import(
`@shopify/shopify-api/rest/admin/${apiVersion}`
);
const shopify = shopifyApi({
// ...
apiVersion,
restResources,
});
// ...
app.get('/api/products/count', async (req, res) => {
const sessionId = await shopify.session.getCurrentId({
isOnline: false,
rawRequest: req,
rawResponse: res,
});
// use sessionId to retrieve session from app's session storage
// getSessionFromStorage() must be provided by application
const session = await getSessionFromStorage(sessionId);
const countData = await shopify.rest.Product.count({session});
res.status(200).send(countData);
});
See using REST resources for more details.