Admin APIobject
Contains objects used to interact with the Admin API.
This object is returned as part of different contexts, such as admin, unauthenticated.admin, and webhook.
Anchor to adminadmin
Provides utilities that apps can use to make requests to the Admin API.
- Anchor to restrestRestClientWithResources<Resources>required
Methods for interacting with the Shopify Admin REST API
There are methods for interacting with individual REST resources. You can also make
,,andrequests should the REST resources not meet your needs.- Anchor to graphqlgraphqlGraphQLClient<AdminOperations>required
Methods for interacting with the Shopify Admin GraphQL API
RestClientWithResources
RemixRestClient & {resources: Resources}RemixRestClient
- session
Session - get
Performs a GET request on the given path.
(params: GetRequestParams) => Promise<Response> - post
Performs a POST request on the given path.
(params: PostRequestParams) => Promise<Response> - put
Performs a PUT request on the given path.
(params: PostRequestParams) => Promise<Response> - delete
Performs a DELETE request on the given path.
(params: GetRequestParams) => Promise<Response>
class RemixRestClient {
public session: Session;
private params: AdminClientOptions['params'];
private handleClientError: AdminClientOptions['handleClientError'];
constructor({params, session, handleClientError}: AdminClientOptions) {
this.params = params;
this.handleClientError = handleClientError;
this.session = session;
}
/**
* Performs a GET request on the given path.
*/
public async get(params: GetRequestParams) {
return this.makeRequest({
method: 'GET' as RequestParams['method'],
...params,
});
}
/**
* Performs a POST request on the given path.
*/
public async post(params: PostRequestParams) {
return this.makeRequest({
method: 'POST' as RequestParams['method'],
...params,
});
}
/**
* Performs a PUT request on the given path.
*/
public async put(params: PutRequestParams) {
return this.makeRequest({
method: 'PUT' as RequestParams['method'],
...params,
});
}
/**
* Performs a DELETE request on the given path.
*/
public async delete(params: DeleteRequestParams) {
return this.makeRequest({
method: 'DELETE' as RequestParams['method'],
...params,
});
}
protected async makeRequest(params: RequestParams): Promise<Response> {
const originalClient = new this.params.api.clients.Rest({
session: this.session,
});
const originalRequest = Reflect.get(originalClient, 'request');
try {
const apiResponse = await originalRequest.call(originalClient, params);
// We use a separate client for REST requests and REST resources because we want to override the API library
// client class to return a Response object instead.
return new Response(JSON.stringify(apiResponse.body), {
headers: apiResponse.headers,
});
} catch (error) {
if (this.handleClientError) {
throw await this.handleClientError({
error,
session: this.session,
params: this.params,
});
} else throw new Error(error);
}
}
}Session
Stores App information from logged in merchants so they can make authenticated requests to the Admin API.
- id
The unique identifier for the session.
string - shop
The Shopify shop domain, such as `example.myshopify.com`.
string - state
The state of the session. Used for the OAuth authentication code flow.
string - isOnline
Whether the access token in the session is online or offline.
boolean - scope
The desired scopes for the access token, at the time the session was created.
string - expires
The date the access token expires.
Date - accessToken
The access token for the session.
string - onlineAccessInfo
Information on the user for the session. Only present for online sessions.
OnlineAccessInfo - isActive
Whether the session is active. Active sessions have an access token that is not expired, and has the given scopes.
(scopes: string | string[] | AuthScopes) => boolean - isScopeChanged
Whether the access token has the given scopes.
(scopes: string | string[] | AuthScopes) => boolean - isExpired
Whether the access token is expired.
(withinMillisecondsOfExpiry?: number) => boolean - toObject
Converts an object with data into a Session.
() => SessionParams - equals
Checks whether the given session is equal to this session.
(other: Session) => boolean - toPropertyArray
Converts the session into an array of key-value pairs.
(returnUserData?: boolean) => [string, string | number | boolean][]
export class Session {
public static fromPropertyArray(
entries: [string, string | number | boolean][],
returnUserData = false,
): Session {
if (!Array.isArray(entries)) {
throw new InvalidSession(
'The parameter is not an array: a Session cannot be created from this object.',
);
}
const obj = Object.fromEntries(
entries
.filter(([_key, value]) => value !== null && value !== undefined)
// Sanitize keys
.map(([key, value]) => {
switch (key.toLowerCase()) {
case 'isonline':
return ['isOnline', value];
case 'accesstoken':
return ['accessToken', value];
case 'onlineaccessinfo':
return ['onlineAccessInfo', value];
case 'userid':
return ['userId', value];
case 'firstname':
return ['firstName', value];
case 'lastname':
return ['lastName', value];
case 'accountowner':
return ['accountOwner', value];
case 'emailverified':
return ['emailVerified', value];
default:
return [key.toLowerCase(), value];
}
}),
);
const sessionData = {} as SessionParams;
const onlineAccessInfo = {
associated_user: {},
} as OnlineAccessInfo;
Object.entries(obj).forEach(([key, value]) => {
switch (key) {
case 'isOnline':
if (typeof value === 'string') {
sessionData[key] = value.toString().toLowerCase() === 'true';
} else if (typeof value === 'number') {
sessionData[key] = Boolean(value);
} else {
sessionData[key] = value;
}
break;
case 'scope':
sessionData[key] = value.toString();
break;
case 'expires':
sessionData[key] = value ? new Date(Number(value)) : undefined;
break;
case 'onlineAccessInfo':
onlineAccessInfo.associated_user.id = Number(value);
break;
case 'userId':
if (returnUserData) {
onlineAccessInfo.associated_user.id = Number(value);
break;
}
case 'firstName':
if (returnUserData) {
onlineAccessInfo.associated_user.first_name = String(value);
break;
}
case 'lastName':
if (returnUserData) {
onlineAccessInfo.associated_user.last_name = String(value);
break;
}
case 'email':
if (returnUserData) {
onlineAccessInfo.associated_user.email = String(value);
break;
}
case 'accountOwner':
if (returnUserData) {
onlineAccessInfo.associated_user.account_owner = Boolean(value);
break;
}
case 'locale':
if (returnUserData) {
onlineAccessInfo.associated_user.locale = String(value);
break;
}
case 'collaborator':
if (returnUserData) {
onlineAccessInfo.associated_user.collaborator = Boolean(value);
break;
}
case 'emailVerified':
if (returnUserData) {
onlineAccessInfo.associated_user.email_verified = Boolean(value);
break;
}
// Return any user keys as passed in
default:
sessionData[key] = value;
}
});
if (sessionData.isOnline) {
sessionData.onlineAccessInfo = onlineAccessInfo;
}
const session = new Session(sessionData);
return session;
}
/**
* The unique identifier for the session.
*/
readonly id: string;
/**
* The Shopify shop domain, such as `example.myshopify.com`.
*/
public shop: string;
/**
* The state of the session. Used for the OAuth authentication code flow.
*/
public state: string;
/**
* Whether the access token in the session is online or offline.
*/
public isOnline: boolean;
/**
* The desired scopes for the access token, at the time the session was created.
*/
public scope?: string;
/**
* The date the access token expires.
*/
public expires?: Date;
/**
* The access token for the session.
*/
public accessToken?: string;
/**
* Information on the user for the session. Only present for online sessions.
*/
public onlineAccessInfo?: OnlineAccessInfo;
constructor(params: SessionParams) {
Object.assign(this, params);
}
/**
* Whether the session is active. Active sessions have an access token that is not expired, and has the given scopes.
*/
public isActive(scopes: AuthScopes | string | string[]): boolean {
return (
!this.isScopeChanged(scopes) &&
Boolean(this.accessToken) &&
!this.isExpired()
);
}
/**
* Whether the access token has the given scopes.
*/
public isScopeChanged(scopes: AuthScopes | string | string[]): boolean {
const scopesObject =
scopes instanceof AuthScopes ? scopes : new AuthScopes(scopes);
return !scopesObject.equals(this.scope);
}
/**
* Whether the access token is expired.
*/
public isExpired(withinMillisecondsOfExpiry = 0): boolean {
return Boolean(
this.expires &&
this.expires.getTime() - withinMillisecondsOfExpiry < Date.now(),
);
}
/**
* Converts an object with data into a Session.
*/
public toObject(): SessionParams {
const object: SessionParams = {
id: this.id,
shop: this.shop,
state: this.state,
isOnline: this.isOnline,
};
if (this.scope) {
object.scope = this.scope;
}
if (this.expires) {
object.expires = this.expires;
}
if (this.accessToken) {
object.accessToken = this.accessToken;
}
if (this.onlineAccessInfo) {
object.onlineAccessInfo = this.onlineAccessInfo;
}
return object;
}
/**
* Checks whether the given session is equal to this session.
*/
public equals(other: Session | undefined): boolean {
if (!other) return false;
const mandatoryPropsMatch =
this.id === other.id &&
this.shop === other.shop &&
this.state === other.state &&
this.isOnline === other.isOnline;
if (!mandatoryPropsMatch) return false;
const copyA = this.toPropertyArray(true);
copyA.sort(([k1], [k2]) => (k1 < k2 ? -1 : 1));
const copyB = other.toPropertyArray(true);
copyB.sort(([k1], [k2]) => (k1 < k2 ? -1 : 1));
return JSON.stringify(copyA) === JSON.stringify(copyB);
}
/**
* Converts the session into an array of key-value pairs.
*/
public toPropertyArray(
returnUserData = false,
): [string, string | number | boolean][] {
return (
Object.entries(this)
.filter(
([key, value]) =>
propertiesToSave.includes(key) &&
value !== undefined &&
value !== null,
)
// Prepare values for db storage
.flatMap(([key, value]): [string, string | number | boolean][] => {
switch (key) {
case 'expires':
return [[key, value ? value.getTime() : undefined]];
case 'onlineAccessInfo':
// eslint-disable-next-line no-negated-condition
if (!returnUserData) {
return [[key, value.associated_user.id]];
} else {
return [
['userId', value?.associated_user?.id],
['firstName', value?.associated_user?.first_name],
['lastName', value?.associated_user?.last_name],
['email', value?.associated_user?.email],
['locale', value?.associated_user?.locale],
['emailVerified', value?.associated_user?.email_verified],
['accountOwner', value?.associated_user?.account_owner],
['collaborator', value?.associated_user?.collaborator],
];
}
default:
return [[key, value]];
}
})
// Filter out tuples with undefined values
.filter(([_key, value]) => value !== undefined)
);
}
}OnlineAccessInfo
- expires_in
How long the access token is valid for, in seconds.
number - associated_user_scope
The effective set of scopes for the session.
string - associated_user
The user associated with the access token.
OnlineAccessUser
export interface OnlineAccessInfo {
/**
* How long the access token is valid for, in seconds.
*/
expires_in: number;
/**
* The effective set of scopes for the session.
*/
associated_user_scope: string;
/**
* The user associated with the access token.
*/
associated_user: OnlineAccessUser;
}OnlineAccessUser
- id
The user's ID.
number - first_name
The user's first name.
string - last_name
The user's last name.
string - email
The user's email address.
string - email_verified
Whether the user has verified their email address.
boolean - account_owner
Whether the user is the account owner.
boolean - locale
The user's locale.
string - collaborator
Whether the user is a collaborator.
boolean
export interface OnlineAccessUser {
/**
* The user's ID.
*/
id: number;
/**
* The user's first name.
*/
first_name: string;
/**
* The user's last name.
*/
last_name: string;
/**
* The user's email address.
*/
email: string;
/**
* Whether the user has verified their email address.
*/
email_verified: boolean;
/**
* Whether the user is the account owner.
*/
account_owner: boolean;
/**
* The user's locale.
*/
locale: string;
/**
* Whether the user is a collaborator.
*/
collaborator: boolean;
}AuthScopes
A class that represents a set of access token scopes.
- has
Checks whether the current set of scopes includes the given one.
(scope: string | string[] | AuthScopes) => boolean - equals
Checks whether the current set of scopes equals the given one.
(otherScopes: string | string[] | AuthScopes) => boolean - toString
Returns a comma-separated string with the current set of scopes.
() => string - toArray
Returns an array with the current set of scopes.
() => any[]
class AuthScopes {
public static SCOPE_DELIMITER = ',';
private compressedScopes: Set<string>;
private expandedScopes: Set<string>;
constructor(scopes: string | string[] | AuthScopes | undefined) {
let scopesArray: string[] = [];
if (typeof scopes === 'string') {
scopesArray = scopes.split(
new RegExp(`${AuthScopes.SCOPE_DELIMITER}\\s*`),
);
} else if (Array.isArray(scopes)) {
scopesArray = scopes;
} else if (scopes) {
scopesArray = Array.from(scopes.expandedScopes);
}
scopesArray = scopesArray
.map((scope) => scope.trim())
.filter((scope) => scope.length);
const impliedScopes = this.getImpliedScopes(scopesArray);
const scopeSet = new Set(scopesArray);
const impliedSet = new Set(impliedScopes);
this.compressedScopes = new Set(
[...scopeSet].filter((x) => !impliedSet.has(x)),
);
this.expandedScopes = new Set([...scopeSet, ...impliedSet]);
}
/**
* Checks whether the current set of scopes includes the given one.
*/
public has(scope: string | string[] | AuthScopes | undefined) {
let other: AuthScopes;
if (scope instanceof AuthScopes) {
other = scope;
} else {
other = new AuthScopes(scope);
}
return (
other.toArray().filter((x) => !this.expandedScopes.has(x)).length === 0
);
}
/**
* Checks whether the current set of scopes equals the given one.
*/
public equals(otherScopes: string | string[] | AuthScopes | undefined) {
let other: AuthScopes;
if (otherScopes instanceof AuthScopes) {
other = otherScopes;
} else {
other = new AuthScopes(otherScopes);
}
return (
this.compressedScopes.size === other.compressedScopes.size &&
this.toArray().filter((x) => !other.has(x)).length === 0
);
}
/**
* Returns a comma-separated string with the current set of scopes.
*/
public toString() {
return this.toArray().join(AuthScopes.SCOPE_DELIMITER);
}
/**
* Returns an array with the current set of scopes.
*/
public toArray() {
return [...this.compressedScopes];
}
private getImpliedScopes(scopesArray: string[]): string[] {
return scopesArray.reduce((array: string[], current: string) => {
const matches = current.match(/^(unauthenticated_)?write_(.*)$/);
if (matches) {
array.push(`${matches[1] ? matches[1] : ''}read_${matches[2]}`);
}
return array;
}, []);
}
}SessionParams
- [key: string]
any - id
The unique identifier for the session.
string - shop
The Shopify shop domain.
string - state
The state of the session. Used for the OAuth authentication code flow.
string - isOnline
Whether the access token in the session is online or offline.
boolean - scope
The scopes for the access token.
string - expires
The date the access token expires.
Date - accessToken
The access token for the session.
string - onlineAccessInfo
Information on the user for the session. Only present for online sessions.
OnlineAccessInfo | StoredOnlineAccessInfo
export interface SessionParams {
/**
* The unique identifier for the session.
*/
readonly id: string;
/**
* The Shopify shop domain.
*/
shop: string;
/**
* The state of the session. Used for the OAuth authentication code flow.
*/
state: string;
/**
* Whether the access token in the session is online or offline.
*/
isOnline: boolean;
/**
* The scopes for the access token.
*/
scope?: string;
/**
* The date the access token expires.
*/
expires?: Date;
/**
* The access token for the session.
*/
accessToken?: string;
/**
* Information on the user for the session. Only present for online sessions.
*/
onlineAccessInfo?: OnlineAccessInfo | StoredOnlineAccessInfo;
/**
* Additional properties of the session allowing for extension
*/
[key: string]: any;
}StoredOnlineAccessInfo
Omit<OnlineAccessInfo, 'associated_user'> & {
associated_user: Partial<OnlineAccessUser>;
}GetRequestParams
- path
The path to the resource, relative to the API version root.
string - type
The type of data expected in the response.
DataType - data
The request body.
string | Record<string, any> - query
Query parameters to be sent with the request.
SearchParams - extraHeaders
Additional headers to be sent with the request.
HeaderParams - tries
The maximum number of times the request can be made if it fails with a throttling or server error.
number
export interface GetRequestParams {
/**
* The path to the resource, relative to the API version root.
*/
path: string;
/**
* The type of data expected in the response.
*/
type?: DataType;
/**
* The request body.
*/
data?: Record<string, any> | string;
/**
* Query parameters to be sent with the request.
*/
query?: SearchParams;
/**
* Additional headers to be sent with the request.
*/
extraHeaders?: HeaderParams;
/**
* The maximum number of times the request can be made if it fails with a throttling or server error.
*/
tries?: number;
}DataType
- JSON
application/json - GraphQL
application/graphql - URLEncoded
application/x-www-form-urlencoded
export enum DataType {
JSON = 'application/json',
GraphQL = 'application/graphql',
URLEncoded = 'application/x-www-form-urlencoded',
}HeaderParams
Headers to be sent with the request.
Record<string, string | number | string[]>PostRequestParams
GetRequestParams & {
data: Record<string, any> | string;
}GraphQLClient
- query
Operation extends keyof Operations - options
GraphQLQueryOptions<Operation, Operations>
interface Promise<T> {
/**
* Attaches callbacks for the resolution and/or rejection of the Promise.
* @param onfulfilled The callback to execute when the Promise is resolved.
* @param onrejected The callback to execute when the Promise is rejected.
* @returns A Promise for the completion of which ever callback is executed.
*/
then<TResult1 = T, TResult2 = never>(onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null): Promise<TResult1 | TResult2>;
/**
* Attaches a callback for only the rejection of the Promise.
* @param onrejected The callback to execute when the Promise is rejected.
* @returns A Promise for the completion of the callback.
*/
catch<TResult = never>(onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null): Promise<T | TResult>;
}, interface Promise<T> {}, Promise: PromiseConstructor, interface Promise<T> {
readonly [Symbol.toStringTag]: string;
}, interface Promise<T> {
/**
* Attaches a callback that is invoked when the Promise is settled (fulfilled or rejected). The
* resolved value cannot be modified from the callback.
* @param onfinally The callback to execute when the Promise is settled (fulfilled or rejected).
* @returns A Promise for the completion of the callback.
*/
finally(onfinally?: (() => void) | undefined | null): Promise<T>;
}export type GraphQLClient<Operations extends AllOperations> = <
Operation extends keyof Operations,
>(
query: Operation,
options?: GraphQLQueryOptions<Operation, Operations>,
) => Promise<GraphQLResponse<Operation, Operations>>;GraphQLQueryOptions
- variables
The variables to pass to the operation.
ApiClientRequestOptions<Operation, Operations>["variables"] - apiVersion
The version of the API to use for the request.
ApiVersion - headers
Additional headers to include in the request.
Record<string, any> - tries
The total number of times to try the request if it fails.
number
export interface GraphQLQueryOptions<
Operation extends keyof Operations,
Operations extends AllOperations,
> {
/**
* The variables to pass to the operation.
*/
variables?: ApiClientRequestOptions<Operation, Operations>['variables'];
/**
* The version of the API to use for the request.
*/
apiVersion?: ApiVersion;
/**
* Additional headers to include in the request.
*/
headers?: Record<string, any>;
/**
* The total number of times to try the request if it fails.
*/
tries?: number;
}ApiVersion
- October22
2022-10 - January23
2023-01 - April23
2023-04 - July23
2023-07 - October23
2023-10 - January24
2024-01 - April24
2024-04 - Unstable
unstable
export enum ApiVersion {
October22 = '2022-10',
January23 = '2023-01',
April23 = '2023-04',
July23 = '2023-07',
October23 = '2023-10',
January24 = '2024-01',
April24 = '2024-04',
Unstable = 'unstable',
}Anchor to examplesExamples
Anchor to example-using-rest-resourcesUsing REST resources
Getting the number of orders in a store using REST resources. Visit the Admin REST API references for examples on using each resource.
Anchor to example-performing-a-get-request-to-the-rest-apiPerforming a GET request to the REST API
Use admin.rest.get to make custom requests to make a request to to the endpoint
Anchor to example-performing-a-post-request-to-the-rest-apiPerforming a POST request to the REST API
Use admin.rest.post to make custom requests to make a request to to the customers.json endpoint to send a welcome email
Using REST resources
Examples
Using REST resources
Description
Getting the number of orders in a store using REST resources. Visit the [Admin REST API references](/docs/api/admin-rest) for examples on using each resource.
/app/routes/**\/*.ts
import { LoaderFunctionArgs, json } from "@remix-run/node"; import { authenticate } from "../shopify.server"; export const loader = async ({ request }: LoaderFunctionArgs) => { const { admin, session, } = await authenticate.admin(request); return json( admin.rest.resources.Order.count({ session }), ); };/app/shopify.server.ts
import { shopifyApp } from "@shopify/shopify-app-remix/server"; import { restResources } from "@shopify/shopify-api/rest/admin/2023-07"; const shopify = shopifyApp({ restResources, // ...etc }); export default shopify; export const authenticate = shopify.authenticate;Performing a GET request to the REST API
Description
Use `admin.rest.get` to make custom requests to make a request to to the `customer/count` endpoint
/app/routes/**\/*.ts
import { LoaderFunctionArgs, json } from "@remix-run/node"; import { authenticate } from "../shopify.server"; export const loader = async ({ request }: LoaderFunctionArgs) => { const { admin, session, } = await authenticate.admin(request); const response = await admin.rest.get({ path: "/customers/count.json", }); const customers = await response.json(); return json({ customers }); };/app/shopify.server.ts
import { shopifyApp } from "@shopify/shopify-app-remix/server"; import { restResources } from "@shopify/shopify-api/rest/admin/2023-04"; const shopify = shopifyApp({ restResources, // ...etc }); export default shopify; export const authenticate = shopify.authenticate;Performing a POST request to the REST API
Description
Use `admin.rest.post` to make custom requests to make a request to to the `customers.json` endpoint to send a welcome email
/app/routes/**\/*.ts
import { LoaderFunctionArgs, json } from "@remix-run/node"; import { authenticate } from "../shopify.server"; export const loader = async ({ request }: LoaderFunctionArgs) => { const { admin, session, } = await authenticate.admin(request); const response = admin.rest.post({ path: "customers/7392136888625/send_invite.json", body: { customer_invite: { to: "new_test_email@shopify.com", from: "j.limited@example.com", bcc: ["j.limited@example.com"], subject: "Welcome to my new shop", custom_message: "My awesome new store", }, }, }); const customerInvite = await response.json(); return json({ customerInvite }); };/app/shopify.server.ts
import { shopifyApp } from "@shopify/shopify-app-remix/server"; import { restResources } from "@shopify/shopify-api/rest/admin/2023-04"; const shopify = shopifyApp({ restResources, // ...etc }); export default shopify; export const authenticate = shopify.authenticate;
Anchor to example-graphqlgraphql
Anchor to example-querying-the-graphql-apiQuerying the GraphQL API
Use admin.graphql to make query / mutation requests.
Anchor to example-handling-graphql-errorsHandling GraphQL errors
Catch errors to see error messages from the API.
Querying the GraphQL API
Examples
Querying the GraphQL API
Description
Use `admin.graphql` to make query / mutation requests.
/app/routes/**\/*.ts
import { ActionFunctionArgs } from "@remix-run/node"; import { authenticate } from "../shopify.server"; export const action = async ({ request }: ActionFunctionArgs) => { const { admin } = await authenticate.admin(request); const response = await admin.graphql( `#graphql mutation populateProduct($input: ProductInput!) { productCreate(input: $input) { product { id } } }`, { variables: { input: { title: "Product Name" }, }, }, ); const productData = await response.json(); return json({ productId: productData.data?.productCreate?.product?.id, }); }/app/shopify.server.ts
import { shopifyApp } from "@shopify/shopify-app-remix/server"; const shopify = shopifyApp({ // ... }); export default shopify; export const authenticate = shopify.authenticate;Handling GraphQL errors
Description
Catch `GraphqlQueryError` errors to see error messages from the API.
/app/routes/**\/*.ts
import { ActionFunctionArgs } from "@remix-run/node"; import { authenticate } from "../shopify.server"; export const action = async ({ request }: ActionFunctionArgs) => { const { admin } = await authenticate.admin(request); try { const response = await admin.graphql( `#graphql query incorrectQuery { products(first: 10) { nodes { not_a_field } } }`, ); return json({ data: await response.json() }); } catch (error) { if (error instanceof GraphqlQueryError) { // error.body.errors: // { graphQLErrors: [ // { message: "Field 'not_a_field' doesn't exist on type 'Product'" } // ] } return json({ errors: error.body?.errors }, { status: 500 }); } return json({ message: "An error occurred" }, { status: 500 }); } }/app/shopify.server.ts
import { shopifyApp } from "@shopify/shopify-app-remix/server"; const shopify = shopifyApp({ // ... }); export default shopify; export const authenticate = shopify.authenticate;