Shopify App React Router — Development Skill
Overview
This skill provides end-to-end guidance for building Shopify embedded apps using:
@shopify/shopify-app-react-routerv1.x (latest)- React Router v7 (framework mode)
- Shopify Admin API 2026-04
- TypeScript
The package wraps authentication (OAuth token exchange, session management), GraphQL Admin API access, webhook verification, billing, and App Bridge integration for React Router apps.
适用场景与快速判断(When to Use This Skill)
> 本技能用于构建 Shopify 嵌入式应用(Embedded App)。动手前先确认是否真的需要它。
✅ 适合使用(Use this when…)
- 你要开发 嵌入在 Shopify Admin 后台 运行的应用(左侧菜单 + App Bridge 导航)。
- 技术栈为 React Router v7(framework mode)+ TypeScript + Node.js。
- 需要服务端处理 OAuth 登录、Session 管理、Admin GraphQL/REST API 调用、Webhook 校验、订阅计费。
- 需要官方
authenticate.admin / webhook / public鉴权封装,而非自己手写 OAuth。
❌ 不适合使用(Do NOT use when…)
- 只是想 改店铺主题/页面(用 Liquid)——请用
shopify-liquid/shopify-theme-pro相关技能。 - 要做 自定义店铺前台(Headless Storefront)——请用
shopify-hydrogen/shopify-storefront-graphql。 - 只想 抓取/分析已有店铺数据(无应用开发)——直接用 Admin API 客户端即可,不需要本套 React Router 脚手架。
- 纯静态/营销页面——普通前端框架即可,无需 Shopify 鉴权。
🔀 替代方案速查(Alternatives)
| 目标 | 推荐技能 / 方案 |
|---|---|
| 嵌入式后台应用(本文) | shopify-app-react-router |
| 主题 / Liquid 定制 | shopify-liquid、shopify-theme-pro |
| 无头店铺前台 | shopify-hydrogen、shopify-storefront-graphql |
| 纯数据查询/运营 | shopify-admin、shopify-admin-api |
🚦 触发信号(何时查阅本技能)
当用户/智能体出现以下意图时,应调用本技能(而不是泛泛去搜 Shopify 文档):
- 「我要做一个 Shopify 后台应用」/「开发嵌入式 App」/「App 要出现在店铺后台左侧菜单」
- 「用 React Router v7 搭 Shopify 应用」/「shopify-app-react-router 怎么用」
- 「怎么接 Shopify Admin API(GraphQL/REST)」/「OAuth 登录 / Session 怎么存」
- 「怎么处理 Webhook 校验」/「怎么做 订阅计费 billing」/「App Bridge 导航怎么配」
- 「应用 部署上线 到 Fly/Railway/Vercel 等」
> 若意图是「改主题/Liquid」「做无头店铺前台」「只查数据」,按上文「❌ 不适合」转对应技能。
快速上手(Quick Start)
前置条件:Node ≥ 18、已创建 Shopify Partner 应用(拿到 API Key / Secret)、一个测试店铺。
# 1) 用官方脚手架创建(含 shopify.server.ts、Prisma、routes 模板)
npm create @shopify/app@latest
# 2) 安装依赖
npm install
# 3) 填写环境变量(.env)
# SHOPIFY_API_KEY=xxx
# SHOPIFY_API_SECRET=xxx
# SHOPIFY_APP_URL=https://<your-tunnel>/ # 开发用 cloudflare tunnel / ngrok
# 4) 本地启动(自带隧道)
npm run dev
# 5) 在 Partner Dashboard 配置 App URL 与 Redirect URL,安装到测试店
最小可运行骨架(核心 3 文件):
- app/shopify.server.ts —— shopifyApp() 配置(见第 1 节)。
- app/routes/app.tsx —— 后台布局 + AppProvider + authenticate.admin(见第 7 节)。
- app/routes/webhooks.tsx —— Webhook 校验(见第 5 节)。
> 完成这 3 步即可在店铺后台看到应用菜单并调用 Admin API。后续按需在 loader/action 中调用 admin.graphql(...)。
Package Architecture
@shopify/shopify-app-react-router
├── /server → shopifyApp(), authenticate, unauthenticated, boundary, ApiVersion, LogSeverity
├── /react → AppProvider (client component for embedded admin UI)
└── /test-helpers → testConfig() for integration tests
Key Exports (from @shopify/shopify-app-react-router/server)
| Export | Purpose |
|---|---|
shopifyApp(config) |
Initialize the Shopify app with credentials and options |
authenticate.admin(request) |
Authenticate admin-embedded requests, returns AdminContext |
authenticate.webhook(request) |
Verify webhook HMAC signatures |
authenticate.public(request) |
Authenticate public (unauthenticated) app proxy/checkout requests |
unauthenticated.admin(shop) |
Background/admin API access without a request context |
boundary.error(error) |
Error boundary compatible with Shopify CSP headers |
boundary.headers(args) |
Headers function for CSP compliance |
ApiVersion |
Enum of all API versions (use ApiVersion.April26) |
LogSeverity |
Log level: Error, Warning, Info, Debug |
Key Exports (from @shopify/shopify-app-react-router/react)
| Export | Purpose |
|---|---|
AppProvider |
Wraps admin routes with App Bridge context (embedded, apiKey) |
1. shopifyApp Configuration
Create /app/shopify.server.ts as the central configuration file:
import {
ApiVersion,
LogSeverity,
shopifyApp,
} from "@shopify/shopify-app-react-router/server";
const shopify = shopifyApp({
// Required
apiKey: process.env.SHOPIFY_API_KEY!,
apiSecretKey: process.env.SHOPIFY_API_SECRET!,
appUrl: process.env.SHOPIFY_APP_URL!,
apiVersion: ApiVersion.April26, // 2026-04
// Optional
scopes: ["read_products", "write_products", "read_orders"],
logger: { level: LogSeverity.Debug },
isEmbeddedApp: true, // default: true
distribution: "AppDistribution.AppStore", // or ShopifyAdmin for custom apps
// Session storage — use Prisma, SQLite, PostgreSQL, MongoDB, Redis, etc.
sessionStorage: new PrismaSessionStorage(prisma),
// Billing — optional, for apps with charges
billing: undefined,
// Future flags
future: { v3_authenticatePublic: true },
});
export default shopify;
export const authenticate = shopify.authenticate;
export const unauthenticated = shopify.unauthenticated;
export const sessionStorage = shopify.sessionStorage;
ApiVersion Enum (relevant for 2026-04)
ApiVersion.April26 // "2026-04"
ApiVersion.January26 // "2026-01"
ApiVersion.October25 // "2025-10"
// ... etc
2. Authenticate Admin Requests
Basic Pattern (Loader / Action)
import { authenticate } from "~/shopify.server";
import type { LoaderFunctionArgs, ActionFunctionArgs } from "react-router";
// In a loader
export const loader = async ({ request }: LoaderFunctionArgs) => {
const { admin, session, billing, cors, redirect } =
await authenticate.admin(request);
// admin → { graphql, rest (via shopify-api) }
// session → { id, shop, accessToken, isOnline, scope, expires }
// billing → { require, check, cancel, request }
// cors → function to wrap cross-origin responses
// redirect → { adminPath, embedApp }
return null;
};
// In an action
export const action = async ({ request }: ActionFunctionArgs) => {
const { admin } = await authenticate.admin(request);
// ...
};
AdminContext Return Type
After authenticate.admin(request), the returned AdminContext contains:
| Property | Type | Description |
|---|---|---|
admin |
AdminApiContext |
Authenticated API client (graphql + rest) |
session |
Session |
Active session: id, shop, accessToken, isOnline, scope, expires |
billing |
BillingContext |
Billing helpers (require, check, cancel, request) |
cors |
(data) => Response |
Wrap responses with CORS headers for cross-origin extensions |
redirect |
AdminRedirect |
Redirect helpers (adminPath, embedApp) |
botOrigin |
string |
Bot protection origin URL |
3. GraphQL Admin API (primary API)
export const action = async ({ request }: ActionFunctionArgs) => {
const { admin } = await authenticate.admin(request);
// admin.graphql(query, options) → Response
const response = await admin.graphql(
`#graphql
query GetProducts($first: Int!) {
products(first: $first) {
nodes {
id
title
variants(first: 5) {
nodes { id price sku inventoryQuantity }
}
}
}
}`,
{
variables: { first: 10 },
// Optional: Retry configuration
// retries: 2,
}
);
const { data, errors } = await response.json();
if (errors) {
console.error("GraphQL errors:", errors);
return { error: "Query failed" };
}
return { products: data.products.nodes };
};
GraphQL Client Details
- URL:
https://{shop}/admin/api/2026-04/graphql.json - Auth: Automatically injected
X-Shopify-Access-Tokenfrom session - Return: Standard
Responseobject — call.json()to parse - #graphql tag: Optional comment for IDE GraphQL syntax highlighting
- Retries: Optional
{ retries: 2 }for automatic retry on rate limits - Typed responses: Use
@shopify/api-codegen-presetfor TypeScript types
Common GraphQL Operations (2026-04)
# Fetch products with variants and inventory
query {
products(first: 50) {
nodes {
id
title
handle
status
vendor
productType
tags
totalInventory
variants(first: 50) {
nodes { id title price sku inventoryQuantity }
}
}
}
}
# Fetch orders with line items
query {
orders(first: 50, reverse: true) {
nodes {
id
name
createdAt
totalPriceSet { shopMoney { amount currencyCode } }
displayFinancialStatus
lineItems(first: 20) {
nodes { title quantity originalUnitPriceSet { shopMoney { amount } } }
}
}
}
}
# Bulk product update
mutation {
productUpdate(input: { id: "gid://shopify/Product/123", title: "New Title" }) {
product { id title }
}
}
4. REST Admin API (via direct fetch)
The package does NOT expose admin.rest() natively in v1. For REST endpoints, use direct fetch with the session access token:
export const action = async ({ request }: ActionFunctionArgs) => {
const { session } = await authenticate.admin(request);
// Direct REST call
const response = await fetch(
`https://${session.shop}/admin/api/2026-04/orders.json?status=any&limit=250`,
{
headers: {
"X-Shopify-Access-Token": session.accessToken!,
"Content-Type": "application/json",
},
}
);
const data = await response.json();
return { orders: data.orders };
};
REST rate limits (2026-04)
- REST: 2 requests/second (leaky bucket, 40 burst)
- GraphQL: 50 points/second (1 point per query cost)
5. Webhook Handling
// /app/routes/webhooks.tsx (NOT nested under admin layout)
export const action = async ({ request }: ActionFunctionArgs) => {
const { topic, shop, payload, admin } =
await authenticate.webhook(request);
console.log(`Webhook: ${topic} from ${shop}`);
// Access the webhook body directly
// payload contains the parsed webhook body
// Optionally use admin API in the handler
if (topic === "PRODUCTS_CREATE") {
// Do something with the product
}
return new Response(null, { status: 200 });
};
Common webhook topics (2026-04)
| Topic | Trigger |
|---|---|
PRODUCTS_CREATE |
Product created |
PRODUCTS_UPDATE |
Product updated |
PRODUCTS_DELETE |
Product deleted |
ORDERS_CREATE |
Order created |
ORDERS_UPDATED |
Order updated |
ORDERS_CANCELLED |
Order cancelled |
ORDERS_FULFILLED |
Order fulfilled |
CUSTOMERS_CREATE |
Customer created |
APP_UNINSTALLED |
App uninstalled (clean up session) |
GDPR_REQUEST |
GDPR data request |
6. Background Jobs (unauthenticated.admin)
For background tasks (cron jobs, queue workers, 3rd-party webhooks):
import { unauthenticated } from "~/shopify.server";
async function processBackgroundJob(shop: string) {
const { admin } = await unauthenticated.admin(shop);
const response = await admin.graphql(
`#graphql
query { products(first: 10) { nodes { id title } } }
`,
);
// Process data...
}
7. Admin Layout & Route Structure
app/
├── shopify.server.ts # shopifyApp() config → default export
├── routes/
│ ├── app.tsx # Admin layout route (auth + AppBridge)
│ ├── app._index.tsx # Admin home page
│ ├── app.products.tsx # Product management page
│ ├── app.orders.tsx # Order management page
│ └── webhooks.tsx # Webhook handler (outside admin layout)
Admin Layout Route (app/routes/app.tsx)
import { Link, Outlet, useLoaderData, useRouteError } from "react-router";
import { boundary } from "@shopify/shopify-app-react-router/server";
import { AppProvider } from "@shopify/shopify-app-react-router/react";
import { NavMenu } from "@shopify/app-bridge-react";
import { authenticate } from "~/shopify.server";
export const loader = async ({ request }) => {
await authenticate.admin(request);
return { apiKey: process.env.SHOPIFY_API_KEY || "" };
};
export default function App() {
const { apiKey } = useLoaderData();
return (
<AppProvider embedded apiKey={apiKey}>
<NavMenu>
<Link to="/app" rel="home">Home</Link>
<Link to="/app/products">Products</Link>
</NavMenu>
<Outlet />
</AppProvider>
);
}
export function ErrorBoundary() {
return boundary.error(useRouteError());
}
export const headers = (args) => boundary.headers(args);
8. Session Storage
Prisma + SQLite (default template)
import { PrismaSessionStorage } from "@shopify/shopify-app-session-storage-prisma";
import prisma from "./db.server";
const shopify = shopifyApp({
// ...
sessionStorage: new PrismaSessionStorage(prisma),
});
Other adapters
| Package | Storage |
|---|---|
@shopify/shopify-app-session-storage-prisma |
Prisma (any DB) |
@shopify/shopify-app-session-storage-postgresql |
PostgreSQL |
@shopify/shopify-app-session-storage-mysql |
MySQL |
@shopify/shopify-app-session-storage-mongodb |
MongoDB |
@shopify/shopify-app-session-storage-redis |
Redis |
@shopify/shopify-app-session-storage-dynamodb |
DynamoDB |
@shopify/shopify-app-session-storage-sqlite |
SQLite |
9. Billing (Subscription Charges)
import { BillingInterval } from "@shopify/shopify-app-react-router/server";
// In shopifyApp config:
const shopify = shopifyApp({
billing: {
["My Plan"]: {
amount: 9.99,
currencyCode: "USD",
interval: BillingInterval.Monthly,
trialDays: 7,
},
},
// ...
});
// In a loader/action:
export const loader = async ({ request }) => {
const { billing, session } = await authenticate.admin(request);
// Check if shop has active subscription
const hasPayment = await billing.check({
plans: ["My Plan"],
isTest: true, // During development
});
if (!hasPayment) {
// Redirect to billing page
const billingUrl = await billing.request({
plan: "My Plan",
isTest: true,
});
return redirect(billingUrl);
}
return { hasPayment };
};
10. Testing
import { testConfig } from "@shopify/shopify-app-react-router/test-helpers";
// In shopify.server.ts:
let config = { apiKey: "...", apiSecretKey: "...", appUrl: "...", apiVersion: ApiVersion.April26 };
if (process.env.SHOPIFY_TESTING) {
Object.assign(config, testConfig());
}
// For e2e testing with real session storage:
if (process.env.SHOPIFY_TESTING === "e2e") {
Object.assign(config, testConfig({ sessionStorage }));
}
11. Deployment
- Supported platforms: Any Node.js host (Fly.io, Railway, Render, Heroku, AWS, Cloudflare Workers with nodejs compat)
- Required env vars:
SHOPIFY_API_KEY,SHOPIFY_API_SECRET,SHOPIFY_APP_URL - Database requirement: Session storage needs a persistent database
- Scopes: Configure in
shopify.app.tomlor Partner Dashboard - Webhooks: Register in
shopify.app.tomlor via Partner Dashboard - GDPR: Implement mandatory webhooks:
customers/data_request,customers/redact,shop/redact
12. Top Shopify Admin API 2026-04 Endpoints
Refer to references/admin-api-2026-04.md for the complete reference of commonly used GraphQL queries, mutations, and REST endpoints for the Shopify Admin API 2026-04 version, including product management, order processing, inventory control, customer data, and store configuration.
13. Security Best Practices(安全最佳实践)
> 嵌入式应用直接持有店铺访问令牌,安全是底线。下列为必做项。
13.1 密钥与凭证管理
SHOPIFY_API_SECRET、accessToken只能存在于服务端环境变量,绝对不要写进前端代码或通过 API 返回给浏览器。- 使用
.env(已 gitignore),禁止提交.env到仓库;CI/CD 用平台密钥注入。 - 轮换 Secret 时同步更新环境变量与 Partner Dashboard。
13.2 Webhook HMAC 校验(必须)
- 永远走
authenticate.webhook(request),它会校验X-Shopify-Hmac-SHA256头。 - 切勿在 webhook 路由里手动
request.json()后直接处理——未经校验的请求可能来自伪造源。 - 处理失败返回
401/500让 Shopify 重试,不要用200吞掉错误。
13.3 Session 与令牌安全
- Session Storage 必须持久化且加密传输;生产环境用 PostgreSQL/Redis,不要用内存存储(多实例会丢 session)。
isOnlinesession 含用户级令牌,避免长期留存;离线任务用unauthenticated.admin(shop)。APP_UNINSTALLEDwebhook 中必须删除该 shop 的所有 session,否则存在令牌永久泄露风险。
13.4 最小权限(Scope)
- 只申请业务必需的 scope(只读就别要
write_*),缩小令牌泄露面。 - 上线前在 Partner Dashboard 复核 scope,避免越权被审核拒绝。
13.5 客户端安全
- 浏览器端只拿到
apiKey(公开信息),绝不下发accessToken。 - 用
boundary.headers()维持 Shopify 要求的 CSP 头,防止 iframe 被劫持。 - 所有对外接口强制 HTTPS;本地开发隧道也用 HTTPS。
13.6 防滥用
- 对自己暴露的 action/loader 做基础限流与参数校验,避免被恶意调用耗尽 Shopify API 配额。
14. Error Handling & Resilience(错误处理与容错)
> 评测指出原文档在「网络波动 / 限流 / 参数错误」时缺少清晰提示。本节给出可复制的容错模式。
14.1 通用包装器(Loader / Action)
import { authenticate } from "~/shopify.server";
// 统一错误处理包装:区分鉴权失败 / 参数错误 / 限流 / 未知
async function withAdmin<T>(
request: Request,
fn: (admin: any, session: any) => Promise<T>
): Promise<Response> {
try {
const { admin, session } = await authenticate.admin(request);
const result = await fn(admin, session);
return Response.json({ ok: true, data: result });
} catch (err) {
const status = (err as any)?.response?.status ?? 500;
console.error("[shopify-app] request failed:", err);
return Response.json(
{ ok: false, error: (err as Error).message, code: status },
{ status: status >= 400 && status < 500 ? status : 500 }
);
}
}
14.2 GraphQL 错误解析
admin.graphql() 即便业务出错也常返回 HTTP 200,错误在 errors 数组里:
const res = await admin.graphql(query, { variables });
const { data, errors } = await res.json();
if (errors?.length) {
// errors: [{ message, path, extensions: { code } }]
// 常见 code: USER_ERROR / THROTTLED / GRAPHQL_VALIDATION_ERROR
console.error("GraphQL errors:", errors);
const msg = errors.map((e: any) => e.message).join("; ");
return Response.json({ ok: false, error: msg }, { status: 422 });
}
14.3 REST 错误处理
const res = await fetch(url, { headers });
if (!res.ok) {
// 429 = 限流, 401 = token 失效, 422 = 参数/业务错误, 404 = 资源不存在
const body = await res.text();
console.error(`REST ${res.status}:`, body);
return Response.json({ ok: false, error: body, code: res.status }, { status: res.status });
}
14.4 限流(Rate Limit)应对
- GraphQL:50 点/秒。超出返回
extensions.code === "THROTTLED"。 - REST:2 次/秒(漏桶,40 突发)。超量返回 HTTP 429,响应头
Retry-After给出等待秒数。 - 策略:捕获 429 → 读
Retry-After→ 指数退避重试;批量任务用游标分页 + 限速;admin.graphql(query, { retries: 2 })开启自动重试。
14.5 网络重试与超时
- 海外 API 偶发抖动:对
fetch包一层超时(AbortController,建议 10s)与最多 3 次指数退避。 - 开发环境访问
*.myshopify.com/admin/api需稳定外网;国内网络不稳时优先部署到海外节点或加代理。
14.6 Webhook 失败
- 处理超时或抛错时返回非 200,让 Shopify 自动重试(约 19 次,指数退避)。
- 保证处理 幂等(同一事件可能多次投递)——用
payload.id去重。
14.7 错误边界
- 后台路由导出
ErrorBoundary调用boundary.error(useRouteError()),保证异常页符合 Shopify CSP,不会白屏。
14.8 错误诊断决策树(从哪里查起)
- 请求直接抛异常 / 重定向登录循环 → 先查 §16「Failed to validate session」:确认 sessionStorage 持久化、
appUrl与回调一致。 - 返回 HTTP 4xx → REST 看状态码(401 令牌失效 / 422 参数 / 404 资源);GraphQL 看
errors[].extensions.code(USER_ERROR=参数 / THROTTLED=限流 / VALIDATION=语法)。 - 返回 429 → 限流,跳 §14.4:读
Retry-After退避,开启retries,降频。 - 偶发超时 / 连不上 → 网络层,跳 §14.5 + §18 国内适配:检查外网/隧道,部署到海外节点或加代理,加 AbortController 超时。
- Webhook 收不到 / 401 → 跳 §16「Webhook 返回 401」:只能用
authenticate.webhook,不要手动 parse。 - 页面白屏 / 无法嵌入 → 跳 §16「应用无法嵌入」:确认
boundary.headers()与isEmbeddedApp: true。
15. Best Practices(最佳实践)
15.1 项目结构约定
- 集中
shopify.server.ts导出authenticate / unauthenticated / sessionStorage。 - 路由按业务域拆分(
app.products.tsx、app.orders.tsx);Webhook 路由 不嵌套 在 admin 布局下。
15.2 游标分页(避免 offset)
query Products($cursor: String) {
products(first: 50, after: $cursor) {
pageInfo { hasNextPage endCursor }
nodes { id title }
}
}
- 用
pageInfo.endCursor循环,直到hasNextPage=false,不要用偏移分页。
15.3 批量操作
- 大批量写入用 Bulk mutation /
bulkOperationRunMutation;导入导出用productImport/ CSV。 - 单次
products(first: N)控制 N,避免单次请求过大触发超时。
15.4 避免过度请求
- 用 GraphQL 精确选择字段,避免
... on Product { * }全量拉取。 - 客户端列表缓存 + 服务端按需请求。
15.5 Webhook 幂等
- 所有 webhook handler 用事件
id做幂等键,防止重复处理导致数据错乱。
15.6 开启 future flags
future: { v3_authenticatePublic: true }等,提前适配破坏性变更,减少升级成本。
15.7 测试策略
- 单元测试用
testConfig()注入假 session;e2e 用testConfig({ sessionStorage })接真库。 - 对
authenticate、graphql、webhook 校验分别写用例。
16. Troubleshooting / 常见问题(FAQ)
| 症状 | 原因 | 解决 |
|---|---|---|
422 Unprocessable Entity 调 Admin API |
库存/必填字段缺失(如新建变体未设 inventoryItem) |
设置 inventory_policy=continue、inventory_management=shopify,并确保 location 级库存已建 |
GraphQL error: Field 'X' doesn't exist |
API 版本字段差异 | 确认用 ApiVersion.April26(2026-04),字段以 references 文档为准 |
Missing required argument: <arg> |
mutation 参数缺失 | 检查 input 对象是否补全必填字段,参考 references 的 mutation 签名 |
Failed to validate session / 登录循环 |
session 存储失效或在线 session 过期 | 确认 sessionStorage 持久化;线上用 Redis/PG;检查 appUrl 与回调一致 |
| Webhook 返回 401 | HMAC 校验失败 | 只能用 authenticate.webhook(request),不要手动 parse body 后再校验 |
| CORS 报错(Embedded 扩展) | 跨域响应缺头 | 用 cors(data) 包裹返回:return cors({ ... }) |
| 应用无法嵌入 / 白屏 | CSP / App Bridge 配置 | 路由导出 headers = (a) => boundary.headers(a);确认 isEmbeddedApp: true |
429 Too Many Requests |
触限流 | 读 Retry-After 退避;开启 retries;降低请求频率 |
| 开发时连不上 Shopify | 国内网络/隧道不稳 | 用稳定 HTTPS 隧道;或把服务部署到海外节点 |
Scope 被拒 / 权限不足 |
申请的 scope 超出业务 | 收窄 scope 并在 Partner Dashboard 复核 |
16.1 反模式(常见错误与根因)
| 反模式 | 为什么会出问题 | 正确做法 |
|---|---|---|
在前端代码/API 返回里暴露 accessToken |
令牌泄露 = 店铺被完全接管 | 只下发 apiKey 到浏览器;令牌仅留服务端 env |
用内存 sessionStorage 上线 |
多实例/重启后 session 丢失,全员重新登录 | 生产用 Redis/PostgreSQL 等持久化存储 |
Webhook 路由手动 request.json() 后直接处理 |
跳过 HMAC 校验,伪造请求可执行 | 永远走 authenticate.webhook(request) |
| Webhook 路由 嵌套 在 admin layout 下 | admin 布局要求鉴权,webhook 无 session 会 401 | webhook 路由独立、不套 admin 布局 |
用 offset/pageInfo 之外的方式分页大表 |
大表越翻越慢甚至超时 | 用 after: $cursor + pageInfo.endCursor 游标分页 |
| 忽略 429 直接重试 | 触发更长时间封禁 | 读 Retry-After 做指数退避;开启 retries |
把 SHOPIFY_API_SECRET 提交进仓库 |
密钥泄露 | .env gitignore + CI 用平台密钥注入 |
| 不实现 GDPR webhook | 上架审核被拒 / 合规风险 | 必做 customers/data_request、customers/redact、shop/redact |
申请过多 write_* scope |
审核被拒、令牌面过大 | 只申请业务必需的最小 scope |
17. Advanced Scenarios(高级场景)
17.1 App Proxy(应用代理)
- 在店铺前台以
myshopify.com/apps/<your-app>暴露服务端路由,用authenticate.public(request)校验来源,适合主题内嵌区块拉取应用数据。
17.2 后台队列任务
- 用 cron(
node-cron/ 平台定时器)调unauthenticated.admin(shop),处理日报、同步等;注意限流与幂等。
17.3 GDPR Webhook(合规必做)
- 必须实现
customers/data_request、customers/redact、shop/redact: customers/data_request:导出该客户数据并返回。customers/redact:删除客户 PII。shop/redact:应用卸载后删除店铺全部关联数据(含 session)。
17.4 多店铺 / 多 Session
sessionStorage以shop为键隔离;离线任务用unauthenticated.admin(shop)按店铺取令牌。
18. 国内适配指南(China Deployment & Network)
> 评测指出「国内适配性」仅 3.8:内容依赖海外服务、国内需访问国外资源。本节给出可落地的适配建议。
18.1 网络与访问
- Shopify 服务(Partner API、
*.myshopify.com、admin/api)均在海外,国内直接访问可能不稳定或被拦截。 - 开发期:用稳定的 HTTPS 隧道(
cloudflared/ngrok)暴露本地,SHOPIFY_APP_URL必须 HTTPS;隧道域名变化后同步更新 Partner Dashboard 的 App URL / Redirect URL。 - 生产期:将应用部署到 海外节点(Fly.io / Railway 美区、AWS us-east-1、Cloudflare 等),让服务端到 Shopify 的调用走海外内网,规避国内出口瓶颈。
- 若必须经由国内出口,可配置出口代理/专线,但需保证令牌传输全程 HTTPS,且代理本身不落盘密钥。
18.2 数据驻留与合规
- Session / 业务数据建议存储在海外或合规区域,避免跨境数据流动带来的合规问题;GDPR webhook(§17.3)必须实现。
- 不要把店铺数据回传国内再做处理,除非已评估数据出境合规。
18.3 依赖与安装
npm install @shopify/*需访问 npm 海外源;国内可配置镜像(如registry.npmmirror.com),但运行时依赖仍走 Shopify 海外 API。- 构建产物(Docker 镜像)建议提前在海外 CI 构建并推送到可达的镜像仓库。
18.4 面向中国商家的特殊说明
- 如果你已在中国开展 Shopify 业务(服务海外店铺),本技能完全适用;瓶颈主要是网络而非代码。
- 大陆商家直接开店受限,通常服务的是 海外店铺;此时按 18.1–18.3 部署到海外节点即可顺畅运行。