The API

Overview

What it is, the three pieces, and why it takes a name.

Storefront JavaScript can never hold an Admin API token — it's a full-store credential and anything in the bundle is public. src/api is a small serverless gateway that holds the secrets instead, reached through a Shopify App Proxy so it lives on your store's own domain.

1import { api, useAPI } from '@/framework'
2
3// The Admin API and a third-party service are the same call.
4const { data } = useAPI(api.GetShop)
5const reviews = useAPI(api.judgeme.listReviews, { productId })

api is generated from src/api/ on every build, so operation names autocomplete and a typo is undefined rather than a 404 in production.

Do you need it?

Probably not yet. The footer newsletter already works through {% form 'customer' %} with no server at all. Reach for this when you hit something the theme genuinely can't do — per-location inventory, writing metafields, reading orders, calling a CRM.

If you skip it, everything else still works. The only feature that depends on it is the Store availability block on the product section, which renders nothing when the gateway isn't reachable.

The three pieces

Setup involves three things that have to know about each other. It goes wrong when one is missing, so it's worth knowing what they are before you start.

PieceWhat it doesWhere
A Shopify appCarries the App Proxy setting and owns the API credentialsshopify-app/
A deploymentRuns the two endpoints, holds the secretsdeploy/netlify/
The themeKnows which path to callTheme settings → API

You need an app because the App Proxy setting lives on an app and nowhere else. You're not building an app in any real sense — it has no code and no interface.

Set up in this order

The order matters: each step produces something the next one needs.

  1. Create the Shopify app → Client ID and secret
  2. Deploy to Netlify → a URL
  3. Connect them → proxy, scopes, install, verify

Budget twenty minutes the first time.

These docs walk through Netlify, which is what ships configured. Nothing here is Netlify-specific — the gateway is standard JavaScript needing only Request/Response, fetch and crypto.subtle, so it runs on Vercel, Cloudflare, Deno, a container, or your own server. Only steps 2 and 3's URL change. See Other platforms.

Why it takes a name, not a query

This is the one design decision worth understanding before you use it.

An App Proxy signature proves a request came through the shop. It says nothing about who sent it. Every visitor to the storefront can call these endpoints, so a raw Admin passthrough would hand each of them the Admin API: read every customer's address, set prices to zero, mint discount codes.

Same for services — a caller-supplied URL turns your deployment into an open proxy with your API keys attached.

So the server owns the query text and the destination. You still write plain GraphQL; the browser just doesn't get to choose it.

Two endpoints

POST /admin.gql{ operation, variables } — runs a named .graphql file
POST /service{ service, operation, variables } — calls a named third-party API

Both verify the App Proxy signature and fail closed if the secret is missing.

Calling it from a component

One hook for everything. useAPI handles the parts that are easy to get wrong — loading and error state, aborting on unmount, and making sure a slow response for the previous variables can't land after a fast one for the current ones:

1import { api, useAPI } from '@/framework'
2
3const { data, loading, error, refetch } = useAPI(
4 api.GetVariantInventory,
5 { id: `gid://shopify/ProductVariant/${variantId}` },
6 { skip: !variantId }
7)

The api registry

Every operation in src/api/operations and every service operation in src/api/services appears on it:

1api.GetShop // an Admin operation
2api.judgeme.listReviews // a service operation

It's regenerated on every build — both npm run build and npm run api:build — from the same files the server's allowlist is built from, so the two cannot drift. It's gitignored, like the component registry.

Three things follow from targets being objects rather than strings:

  • Names autocomplete in any editor, with no TypeScript.
  • A typo fails immediately. api.GetShopp is undefined, and useAPI throws saying so — rather than sending a string that comes back a 404 from production.
  • No guessing from the shape of a name. Each target states whether it is an Admin operation or a service, so nothing has to infer it from a dot.

A bare string still works if you need one — a name built at runtime, say. It's resolved the old way: a dot means a service.

1useAPI('GetShop') // Admin
2useAPI('judgeme.listReviews', { id }) // service
Option
skipDon't fetch — for a value that isn't ready yet
cacheKeep the result for the page. Off by default: stock and prices go stale, and a wrong number is worse than a second request
onSuccess / onErrorCallbacks; identity changes don't refetch

Pass variables as an inline object — they're compared by value, and key order doesn't matter, so { a, b } and { b, a } are one request. Identical in-flight requests are always deduplicated, so two components asking for the same product produce one round trip.

Writes

A write belongs on an event, not on render, so it gets its own hook:

1const subscribe = useAPIAction(api.SubscribeCustomer)
2
3const { error } = await subscribe.run({ email }) // resolves, never rejects

run never rejects, so an unhandled rejection can't escape an event handler, and it exposes loading and error for the button's state.

For something that is more than one request, pass a function instead:

1const checkout = useAPIAction(async (email) => {
2 const { customerCreate } = await admin('CreateCustomer', { email })
3 return admin('TagCustomer', { id: customerCreate.customer.id })
4})

Outside React — a prefetch on hover, a module-level warm-up — fetchApi gives the same dedup and caching without the hook.

Local development

npm run api:dev # http://localhost:3000

Operations and services regenerate per request, so adding a file takes effect without a restart. Set SHOPIFY_SKIP_PROXY_CHECK=1 in .env.local to call routes directly with curl.

Escape hatches

SHOPIFY_ALLOW_RAW_GQL=1 makes /admin.gql accept a raw query field. It's for local work and server-to-server calls. Setting it on a storefront deployment is equivalent to publishing your Admin token.

SHOPIFY_SKIP_PROXY_CHECK=1 disables signature verification.

Both are read from the environment rather than inferred from NODE_ENV, so neither can switch itself on in a deployment by accident.