--- title: App proxy description: |- [App proxies](/docs/apps/online-store/app-proxies) take requests to Shopify links, and redirect them to external links. The `authenticate.public.appProxy` function validates requests made to app proxies, and returns a context to enable querying Shopify APIs. > Note: If the store has not installed the app, store-related properties such as `admin` or `storefront` will be `undefined` api_version: v2 api_name: shopify-app-remix source_url: html: https://shopify.dev/docs/api/shopify-app-remix/v2/authenticate/public/app-proxy md: https://shopify.dev/docs/api/shopify-app-remix/v2/authenticate/public/app-proxy.md --- # App proxyobject [App proxies](https://shopify.dev/docs/apps/online-store/app-proxies) take requests to Shopify links, and redirect them to external links. The `authenticate.public.appProxy` function validates requests made to app proxies, and returns a context to enable querying Shopify APIs. Note If the store has not installed the app, store-related properties such as `admin` or `storefront` will be `undefined` ## authenticate.​public.​app​Proxy([request](#authenticatepublicappproxy-propertydetail-request)​) Authenticates requests coming to the app from Shopify app proxies. ### Parameters * request Request required ### Returns * Promise\ ### AppProxyContext * session No session is available for the shop that made this request. This comes from the session storage which \`shopifyApp\` uses to store sessions in your database of choice. ```ts undefined ``` * admin No session is available for the shop that made this request. Therefore no methods for interacting with the GraphQL / REST Admin APIs are available. ```ts undefined ``` * storefront No session is available for the shop that made this request. Therefore no method for interacting with the Storefront API is available. ```ts undefined ``` * liquid A utility for creating a Liquid Response. ```ts LiquidResponseFunction ``` ```ts export interface AppProxyContext extends Context { /** * No session is available for the shop that made this request. * * This comes from the session storage which `shopifyApp` uses to store sessions in your database of choice. */ session: undefined; /** * No session is available for the shop that made this request. * Therefore no methods for interacting with the GraphQL / REST Admin APIs are available. */ admin: undefined; /** * No session is available for the shop that made this request. * Therefore no method for interacting with the Storefront API is available. */ storefront: undefined; } ``` ### LiquidResponseFunction * body ```ts string ``` * initAndOptions ```ts number | (ResponseInit & Options) ``` Response ```ts Response ``` ```ts export type LiquidResponseFunction = ( body: string, initAndOptions?: number | (ResponseInit & Options), ) => Response; ``` ### Options * layout Whether to use the shop's theme layout around the Liquid content. ```ts boolean ``` ```ts interface Options { /** * Whether to use the shop's theme layout around the Liquid content. */ layout?: boolean; } ``` ### AppProxyContextWithSession * session The session for the shop that made the request. This comes from the session storage which \`shopifyApp\` uses to store sessions in your database of choice. Use this to get shop or user-specific data. ```ts Session ``` * admin Methods for interacting with the GraphQL / REST Admin APIs for the store that made the request. ```ts AdminApiContext ``` * storefront Method for interacting with the Shopify Storefront Graphql API for the store that made the request. ```ts StorefrontContext ``` * liquid A utility for creating a Liquid Response. ```ts LiquidResponseFunction ``` ````ts export interface AppProxyContextWithSession< Resources extends ShopifyRestResources = ShopifyRestResources, > extends Context { /** * The session for the shop that made the request. * * This comes from the session storage which `shopifyApp` uses to store sessions in your database of choice. * * Use this to get shop or user-specific data. * * @example * Using the session object. * Get the session for the shop that initiated the request to the app proxy. * ```ts * // app/routes/**\/.ts * import { json } from "@remix-run/node"; * import { authenticate } from "../shopify.server"; * import { getMyAppModelData } from "~/db/model.server"; * * export const loader = async ({ request }) => { * // Get the session for the shop that initiated the request to the app proxy. * const { session } = * await authenticate.public.appProxy(request); * * // Use the session data to make to queries to your database or additional requests. * return json( * await getMyAppModelData({shop: session.shop}) * ); * }; * ``` */ session: Session; /** * Methods for interacting with the GraphQL / REST Admin APIs for the store that made the request. * * @example * Interacting with the Admin API. * Use the `admin` object to interact with the REST or GraphQL APIs. * ```ts * // app/routes/**\/.ts * import { json } from "@remix-run/node"; * import { authenticate } from "../shopify.server"; * * export async function action({ request }: ActionFunctionArgs) { * const { admin } = await authenticate.public.appProxy(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({ data: productData.data }); * } * ``` */ admin: AdminApiContext; /** * Method for interacting with the Shopify Storefront Graphql API for the store that made the request. * * @example * Interacting with the Storefront API. * Use the `storefront` object to interact with the GraphQL API. * ```ts * // app/routes/**\/.ts * import { json } from "@remix-run/node"; * import { authenticate } from "../shopify.server"; * * export async function action({ request }: ActionFunctionArgs) { * const { storefront } = await authenticate.public.appProxy(request); * * const response = await storefront.graphql( * `#graphql * query blogIds { * blogs(first: 10) { * edges { * node { * id * } * } * } * }` * ); * * return json(await response.json()); * } * ``` */ storefront: StorefrontContext; } ```` ### 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. ```ts string ``` * shop The Shopify shop domain, such as \`example.myshopify.com\`. ```ts string ``` * state The state of the session. Used for the OAuth authentication code flow. ```ts string ``` * isOnline Whether the access token in the session is online or offline. ```ts boolean ``` * scope The desired scopes for the access token, at the time the session was created. ```ts string ``` * expires The date the access token expires. ```ts Date ``` * accessToken The access token for the session. ```ts string ``` * onlineAccessInfo Information on the user for the session. Only present for online sessions. ```ts OnlineAccessInfo ``` * isActive Whether the session is active. Active sessions have an access token that is not expired, and has the given scopes. ```ts (scopes: string | string[] | AuthScopes) => boolean ``` * isScopeChanged Whether the access token has the given scopes. ```ts (scopes: string | string[] | AuthScopes) => boolean ``` * isExpired Whether the access token is expired. ```ts (withinMillisecondsOfExpiry?: number) => boolean ``` * toObject Converts an object with data into a Session. ```ts () => SessionParams ``` * equals Checks whether the given session is equal to this session. ```ts (other: Session) => boolean ``` * toPropertyArray Converts the session into an array of key-value pairs. ```ts (returnUserData?: boolean) => [string, string | number | boolean][] ``` ```ts 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. ```ts number ``` * associated\_user\_scope The effective set of scopes for the session. ```ts string ``` * associated\_user The user associated with the access token. ```ts OnlineAccessUser ``` ```ts 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. ```ts number ``` * first\_name The user's first name. ```ts string ``` * last\_name The user's last name. ```ts string ``` * email The user's email address. ```ts string ``` * email\_verified Whether the user has verified their email address. ```ts boolean ``` * account\_owner Whether the user is the account owner. ```ts boolean ``` * locale The user's locale. ```ts string ``` * collaborator Whether the user is a collaborator. ```ts boolean ``` ```ts 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. ```ts (scope: string | string[] | AuthScopes) => boolean ``` * equals Checks whether the current set of scopes equals the given one. ```ts (otherScopes: string | string[] | AuthScopes) => boolean ``` * toString Returns a comma-separated string with the current set of scopes. ```ts () => string ``` * toArray Returns an array with the current set of scopes. ```ts () => any[] ``` ```ts class AuthScopes { public static SCOPE_DELIMITER = ','; private compressedScopes: Set; private expandedScopes: Set; 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] ```ts any ``` * id The unique identifier for the session. ```ts string ``` * shop The Shopify shop domain. ```ts string ``` * state The state of the session. Used for the OAuth authentication code flow. ```ts string ``` * isOnline Whether the access token in the session is online or offline. ```ts boolean ``` * scope The scopes for the access token. ```ts string ``` * expires The date the access token expires. ```ts Date ``` * accessToken The access token for the session. ```ts string ``` * onlineAccessInfo Information on the user for the session. Only present for online sessions. ```ts OnlineAccessInfo | StoredOnlineAccessInfo ``` ```ts 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 ```ts Omit & { associated_user: Partial; } ``` ### AdminApiContext * rest Methods for interacting with the Shopify Admin REST API There are methods for interacting with individual REST resources. You can also make \`GET\`, \`POST\`, \`PUT\` and \`DELETE\` requests should the REST resources not meet your needs. ```ts RestClientWithResources ``` * graphql Methods for interacting with the Shopify Admin GraphQL API ```ts GraphQLClient ``` ````ts export interface AdminApiContext< Resources extends ShopifyRestResources = ShopifyRestResources, > { /** * Methods for interacting with the Shopify Admin REST API * * There are methods for interacting with individual REST resources. You can also make `GET`, `POST`, `PUT` and `DELETE` requests should the REST resources not meet your needs. * * {@link https://shopify.dev/docs/api/admin-rest} * * @example * Using REST resources. * 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. * * ```ts * // /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 }), * ); * }; * ``` * * ```ts * // /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; * ``` * * @example * Performing a GET request to the REST API. * Use `admin.rest.get` to make custom requests to make a request to to the `customer/count` endpoint * * ```ts * // /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 }); * }; * ``` * * ```ts * // /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; * ``` * * @example * Performing 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 * ```ts * // /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 }); * }; * ``` * * ```ts * // /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; * ``` */ rest: RestClientWithResources; /** * Methods for interacting with the Shopify Admin GraphQL API * * {@link https://shopify.dev/docs/api/admin-graphql} * {@link https://github.com/Shopify/shopify-app-js/blob/main/packages/apps/shopify-api/docs/reference/clients/Graphql.md} * * @example * Querying the GraphQL API. * Use `admin.graphql` to make query / mutation requests. * ```ts * // /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, * }); * } * ``` * * ```ts * // /app/shopify.server.ts * import { shopifyApp } from "@shopify/shopify-app-remix/server"; * * const shopify = shopifyApp({ * // ... * }); * export default shopify; * export const authenticate = shopify.authenticate; * ``` * * @example * Handling GraphQL errors. * Catch `GraphqlQueryError` errors to see error messages from the API. * ```ts * // /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 }); * } * } * ``` * * ```ts * // /app/shopify.server.ts * import { shopifyApp } from "@shopify/shopify-app-remix/server"; * * const shopify = shopifyApp({ * // ... * }); * export default shopify; * export const authenticate = shopify.authenticate; * ``` */ graphql: GraphQLClient; } ```` ### RestClientWithResources ```ts RemixRestClient & {resources: Resources} ``` ### RemixRestClient * session ```ts Session ``` * get Performs a GET request on the given path. ```ts (params: GetRequestParams) => Promise ``` * post Performs a POST request on the given path. ```ts (params: PostRequestParams) => Promise ``` * put Performs a PUT request on the given path. ```ts (params: PostRequestParams) => Promise ``` * delete Performs a DELETE request on the given path. ```ts (params: GetRequestParams) => Promise ``` ```ts 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 { 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); } } } ``` ### GetRequestParams * path The path to the resource, relative to the API version root. ```ts string ``` * type The type of data expected in the response. ```ts DataType ``` * data The request body. ```ts string | Record ``` * query Query parameters to be sent with the request. ```ts SearchParams ``` * extraHeaders Additional headers to be sent with the request. ```ts HeaderParams ``` * tries The maximum number of times the request can be made if it fails with a throttling or server error. ```ts number ``` ```ts 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; /** * 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 ```ts application/json ``` * GraphQL ```ts application/graphql ``` * URLEncoded ```ts application/x-www-form-urlencoded ``` ```ts export enum DataType { JSON = 'application/json', GraphQL = 'application/graphql', URLEncoded = 'application/x-www-form-urlencoded', } ``` ### HeaderParams Headers to be sent with the request. ```ts Record ``` ### PostRequestParams ```ts GetRequestParams & { data: Record | string; } ``` ### GraphQLClient * query ```ts Operation extends keyof Operations ``` * options ```ts GraphQLQueryOptions ``` interface Promise\ { /\*\* \* 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\(onfulfilled?: ((value: T) => TResult1 | PromiseLike\) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike\) | undefined | null): Promise\; /\*\* \* 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\(onrejected?: ((reason: any) => TResult | PromiseLike\) | undefined | null): Promise\; }, interface Promise\ {}, Promise: PromiseConstructor, interface Promise\ { readonly \[Symbol.toStringTag]: string; }, interface Promise\ { /\*\* \* 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\; } ```ts interface Promise { /** * 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(onfulfilled?: ((value: T) => TResult1 | PromiseLike) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null): Promise; /** * 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(onrejected?: ((reason: any) => TResult | PromiseLike) | undefined | null): Promise; }, interface Promise {}, Promise: PromiseConstructor, interface Promise { readonly [Symbol.toStringTag]: string; }, interface Promise { /** * 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; } ``` ```ts export type GraphQLClient = < Operation extends keyof Operations, >( query: Operation, options?: GraphQLQueryOptions, ) => Promise>; ``` ### GraphQLQueryOptions * variables The variables to pass to the operation. ```ts ApiClientRequestOptions["variables"] ``` * apiVersion The version of the API to use for the request. ```ts ApiVersion ``` * headers Additional headers to include in the request. ```ts Record ``` * tries The total number of times to try the request if it fails. ```ts number ``` ```ts export interface GraphQLQueryOptions< Operation extends keyof Operations, Operations extends AllOperations, > { /** * The variables to pass to the operation. */ variables?: ApiClientRequestOptions['variables']; /** * The version of the API to use for the request. */ apiVersion?: ApiVersion; /** * Additional headers to include in the request. */ headers?: Record; /** * The total number of times to try the request if it fails. */ tries?: number; } ``` ### ApiVersion * October22 ```ts 2022-10 ``` * January23 ```ts 2023-01 ``` * April23 ```ts 2023-04 ``` * July23 ```ts 2023-07 ``` * October23 ```ts 2023-10 ``` * January24 ```ts 2024-01 ``` * April24 ```ts 2024-04 ``` * Unstable ```ts unstable ``` ```ts 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', } ``` ### StorefrontContext * graphql Method for interacting with the Shopify Storefront GraphQL API If you're getting incorrect type hints in the Shopify template, follow \[these instructions]\(https\://github.com/Shopify/shopify-app-template-remix/tree/main#incorrect-graphql-hints). ```ts GraphQLClient ``` ````ts export interface StorefrontContext { /** * Method for interacting with the Shopify Storefront GraphQL API * * If you're getting incorrect type hints in the Shopify template, follow [these instructions](https://github.com/Shopify/shopify-app-template-remix/tree/main#incorrect-graphql-hints). * * {@link https://shopify.dev/docs/api/storefront} * * @example * Querying the GraphQL API. * Use `storefront.graphql` to make query / mutation requests. * ```ts * // app/routes/**\/.ts * import { json } from "@remix-run/node"; * import { authenticate } from "../shopify.server"; * * export async function action({ request }: ActionFunctionArgs) { * const { storefront } = await authenticate.public.appProxy(request); * * const response = await storefront.graphql(`{blogs(first: 10) { edges { node { id } } } }`); * * return json(await response.json()); * } * ``` * * @example * Handling GraphQL errors. * Catch `GraphqlQueryError` errors to see error messages from the API. * ```ts * // /app/routes/**\/*.ts * import { ActionFunctionArgs } from "@remix-run/node"; * import { authenticate } from "../shopify.server"; * * export const action = async ({ request }: ActionFunctionArgs) => { * const { storefront } = await authenticate.public.appProxy(request); * * try { * const response = await storefront.graphql( * `#graphql * query incorrectQuery { * products(first: 10) { * nodes { * not_a_field * } * } * }`, * ); * * return json({ data: await response.json() }); * } catch (error) { * if (error instanceof GraphqlQueryError) { * // { 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 }); * } * } * ``` * * ```ts * // /app/shopify.server.ts * import { shopifyApp } from "@shopify/shopify-app-remix/server"; * * const shopify = shopifyApp({ * // ... * }); * export default shopify; * export const authenticate = shopify.authenticate; * ``` */ graphql: GraphQLClient; } ```` ### Examples * #### Authenticate and fetch product information ##### Description Authenticate and fetch product information ##### /app/routes/\*\*.ts ```typescript import type {LoaderFunctionArgs} from '@remix-run/node'; import {authenticate} from '../shopify.server'; export const loader = async ({request}: LoaderFunctionArgs) => { const {storefront, liquid} = await authenticate.public.appProxy(request); if (!storefront) { return new Response(); } const response = await storefront.graphql( `#graphql query productTitle { products(first: 1) { nodes { title } } }`, ); const body = await response.json(); const title = body.data.products.nodes[0].title; return liquid(`Found product ${title} from {{shop.name}}`); }; ``` ## Examples ### session Using the session object Get the session for the shop that initiated the request to the app proxy. ### Examples * #### Using the session object ##### Description Get the session for the shop that initiated the request to the app proxy. ##### app/routes/\*\*\\/.ts ```typescript import { json } from "@remix-run/node"; import { authenticate } from "../shopify.server"; import { getMyAppModelData } from "~/db/model.server"; export const loader = async ({ request }) => { // Get the session for the shop that initiated the request to the app proxy. const { session } = await authenticate.public.appProxy(request); // Use the session data to make to queries to your database or additional requests. return json( await getMyAppModelData({shop: session.shop}) ); }; ``` ### admin Interacting with the Admin API Use the `admin` object to interact with the REST or GraphQL APIs. ### Examples * #### Interacting with the Admin API ##### Description Use the \`admin\` object to interact with the REST or GraphQL APIs. ##### app/routes/\*\*\\/.ts ```typescript import { json } from "@remix-run/node"; import { authenticate } from "../shopify.server"; export async function action({ request }: ActionFunctionArgs) { const { admin } = await authenticate.public.appProxy(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({ data: productData.data }); } ``` ### storefront Interacting with the Storefront API Use the `storefront` object to interact with the GraphQL API. ### Examples * #### Interacting with the Storefront API ##### Description Use the \`storefront\` object to interact with the GraphQL API. ##### app/routes/\*\*\\/.ts ```typescript import { json } from "@remix-run/node"; import { authenticate } from "../shopify.server"; export async function action({ request }: ActionFunctionArgs) { const { storefront } = await authenticate.public.appProxy(request); const response = await storefront.graphql( `#graphql query blogIds { blogs(first: 10) { edges { node { id } } } }` ); return json(await response.json()); } ``` ### liquid Rendering liquid content Use the `liquid` helper to render a `Response` with Liquid content using the shop's theme. See the [Liquid reference](https://shopify.dev/docs/api/liquid) for all the features it enables. Rendering liquid content without a layout Set the `layout` option to `false` to render the Liquid content without a theme. Rendering a form in a Liquid response Handle form submissions through an app proxy. ### Examples * #### Rendering liquid content ##### Description Use the \`liquid\` helper to render a \`Response\` with Liquid content using the shop's theme. See the \[Liquid reference]\(https\://shopify.dev/docs/api/liquid) for all the features it enables. ##### /app/routes/\*\*\\/\*.ts ```typescript import {authenticate} from "~/shopify.server" export async function loader({ request }) { const {liquid} = await authenticate.public.appProxy(request); return liquid("Hello {{shop.name}}"); } ``` * #### Rendering liquid content without a layout ##### Description Set the \`layout\` option to \`false\` to render the Liquid content without a theme. ##### /app/routes/\*\*\\/\*.ts ```typescript import {authenticate} from "~/shopify.server" export async function loader({ request }) { const {liquid} = await authenticate.public.appProxy(request); return liquid( "Hello {{shop.name}}", { layout: false } ); } ``` * #### Rendering a form in a Liquid response ##### Description Handle form submissions through an app proxy. ##### app/routes/apps.proxy.my-action.tsx ```typescript import { redirect } from "@remix-run/node"; import { authenticate } from "~/shopify.server"; export async function loader({ request }) { const { liquid } = await authenticate.public.appProxy(request); return liquid(`
`); } export async function action({ request }) { await authenticate.public.appProxy(request); const formData = await request.formData(); const field = formData.get("field")?.toString(); // Perform actions here if (field) { console.log("Field:", field); } // Return to the form page return redirect("/apps/proxy/my-action"); } ``` ## Related [![](https://shopify.dev/images/icons/32/pickaxe-1.png)![](https://shopify.dev/images/icons/32/pickaxe-1-dark.png)](https://shopify.dev/docs/api/shopify-app-remix/apis/admin-api) [Interact with the Admin API.Admin API context](https://shopify.dev/docs/api/shopify-app-remix/apis/admin-api) [![](https://shopify.dev/images/icons/32/pickaxe-1.png)![](https://shopify.dev/images/icons/32/pickaxe-1-dark.png)](https://shopify.dev/docs/api/shopify-app-remix/apis/storefront-api) [Interact with the Storefront API.Storefront API context](https://shopify.dev/docs/api/shopify-app-remix/apis/storefront-api) [![](https://shopify.dev/images/icons/32/liquid.png)![](https://shopify.dev/images/icons/32/liquid-dark.png)](https://shopify.dev/docs/api/liquid) [Use the shop's theme to render a template.Liquid reference](https://shopify.dev/docs/api/liquid)