Skip to content

Add proxy routes

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 Shopify app configuration, point the app proxy destination at the public OpenShop /proxy/... route. Shopify owns the storefront prefix, subpath, and TOML schema; use the fields supported by your Shopify CLI 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.