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'23// 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.
| Piece | What it does | Where |
|---|---|---|
| A Shopify app | Carries the App Proxy setting and owns the API credentials | shopify-app/ |
| A deployment | Runs the two endpoints, holds the secrets | deploy/netlify/ |
| The theme | Knows which path to call | Theme 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.
- Create the Shopify app → Client ID and secret
- Deploy to Netlify → a URL
- 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,fetchandcrypto.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'23const { 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 operation2api.judgeme.listReviews // a service operationIt'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.GetShoppisundefined, anduseAPIthrows 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') // Admin2useAPI('judgeme.listReviews', { id }) // service| Option | |
|---|---|
skip | Don't fetch — for a value that isn't ready yet |
cache | Keep the result for the page. Off by default: stock and prices go stale, and a wrong number is worse than a second request |
onSuccess / onError | Callbacks; 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)23const { error } = await subscribe.run({ email }) // resolves, never rejectsrun 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:3000Operations 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.