Skip to content

Add proxy routes

Use this guide to add an endpoint to an existing app for a storefront or Customer Account extension. Have the app installed on a development store. For callers outside Shopify, use Add a server route.

OpenShop discovers route handlers in proxy/ and mounts each one under both /proxy/* and /ext/*.

Create proxy/api/reviews.ts:

import { app } from '#app'
export default app.defineProxy({
type: 'json',
async GET({ shop, customerId, query }) {
return { shop, customerId, page: Number(query.page) || 1 }
},
async POST({ shop, customerId, body }) {
if (!customerId) return { ok: false, error: 'Customer login required' }
return { ok: true, shop, customerId, review: body }
},
})

Supported handlers are GET, POST, PUT, DELETE, and PATCH. Non-GET bodies are parsed as JSON; invalid JSON becomes undefined.

proxy/index.ts -> /proxy and /ext
proxy/reviews.ts -> /proxy/reviews and /ext/reviews
proxy/products/[id].ts -> /proxy/products/:id and /ext/products/:id

Prefix helper files or directories with _:

  • Directoryproxy/
    • Directoryloyalty/
      • _service.ts
      • _queries.ts
      • Directory_shared/
        • index.ts

Only .ts and .js route files are loaded.

In the linked Shopify app TOML, configure the destination and storefront path:

[app_proxy]
url = "/proxy"
prefix = "apps"
subpath = "openshop"

Include Shopify’s required write_app_proxy permission in the app’s requested scopes and complete renewed scope authorization when required. With this mapping, the route from step 1 is reachable through https://<shop>/apps/openshop/api/reviews. Verify the fields against Shopify’s app proxy configuration for your app configuration version.

Deploy the configuration:

Terminal window
shopify app deploy --config shopify.app.toml

Adding a file under proxy/ does not register the app proxy in Shopify.

Enable direct network access in the extension TOML:

[extensions.capabilities]
network_access = true

Call the /ext mount on your app origin:

const token = await shopify.sessionToken.get()
const response = await fetch(`${apiOrigin}/ext/api/reviews`, {
headers: {
Authorization: `Bearer ${token}`,
},
})

/ext/* requires a valid Customer Account session JWT. /proxy/* accepts that JWT too, but /ext avoids Shopify CLI proxy interception.

For storefront app proxy traffic, Shopify signs the query and OpenShop verifies it against exactly one configured app secret. For extension traffic, OpenShop verifies the JWT audience and signature.

Trust ctx.shop, ctx.shopifyApp, and ctx.customerId; do not accept identity from unsanitized client fields. A missing/invalid credential returns HTTP 401.

Shopify extension, admin, storefront, local, HOST, and SHOPIFY_APP_URL origins receive CORS headers. Arbitrary origins do not.

Handler exceptions are logged and return { "error": "Internal proxy error" } with HTTP 500. A successful handler value is always HTTP 200; the current proxy API does not expose custom status or response headers.

Open /apps/openshop/api/reviews?page=2 on the development storefront. Expect a JSON response with the verified shop and page: 2. An unauthenticated request directly to the OpenShop /proxy/api/reviews URL should return 401. Run pnpm run lint before deploying. See the proxy reference for all methods and response limitations.