---
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 proxy
[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.appProxy(**[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
### 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}}`);
};
```
* #### 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})
);
};
```
* #### 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 });
}
```
* #### 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());
}
```
* #### 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
[Interact with the Admin API. - Admin API context](https://shopify.dev/docs/api/shopify-app-remix/apis/admin-api)
[Interact with the Storefront API. - Storefront API context](https://shopify.dev/docs/api/shopify-app-remix/apis/storefront-api)
[Use the shop's theme to render a template. - Liquid reference](https://shopify.dev/docs/api/liquid)