nextjs-store
Get the code
Browse it on GitHub · Download the repository as a zip
git clone https://github.com/JacLight/appmint-examples.git
cd appmint-examples/nextjs-storeA small Next.js 15 storefront that renders your own catalog, shows a product page, and keeps a cart. It is the shape almost every web app on AppEngine should take, and it exists mostly to demonstrate one boundary properly.
The boundary it teaches
Browser ──▶ this app's /api/* ──▶ AppEngineThe browser never talks to AppEngine. It talks to this app's own origin, and the server relays the call with the application credentials attached.
That is not ceremony. Anything in browser JavaScript is readable by anyone who
opens devtools, so an app token that reaches a bundle is an app token you have
published. The comment at the top of lib/appmint.ts states the rule the whole
file follows:
In the browser → calls this app's own
/api/*proxy. No credentials. On the server → calls appengine directly, with the app token attached.
One client, two behaviours, decided by typeof window.
How it is laid out
| File | What it does |
|---|---|
lib/appmint.ts | The client. Fetches the app token once per process, reuses it, renews on a 401 |
app/api/[...slug]/route.ts | The proxy the browser talks to, with an allowlist |
app/page.tsx | The catalog, rendered on the server |
app/product/[sku]/page.tsx | A product page |
components/CartView.tsx | The cart |
lib/cart-storage.ts | Cart kept in the browser between visits |
The proxy is deliberately thin
app/api/[...slug]/route.ts turns GET /api/storefront/products into
GET storefront/products against AppEngine, attaching the app token on the
server side. It allows a fixed list of prefixes:
const ALLOWED = [
'storefront/', 'profile/customer/', 'profile/magic-link',
'affiliate/public/', 'client/', 'shipping/', 'notice/',
];Its own comment is the important part:
It is deliberately thin — it moves credentials off the browser, it does not decide what a visitor may do. Guard anything sensitive in its own route rather than letting it fall through here.
Worth taking seriously. A proxy that forwards anything is a proxy that forwards the thing you did not think about. Anything scoped to a person, or that decides money, deserves a route of its own where your server decides what to ask.
Money comes from the server
The catalog renders prices exactly as AppEngine returns them, and totals come from the pricing endpoint rather than arithmetic in the page. A client that adds up its own subtotal is offering a second opinion, and the moment a price rule or tax rule resolves differently on the server, it quietly shows a number nobody will honour. See money is decided by the server.
Running it
cd appmint-examples/nextjs-store
cp .env.example .env.local # fill in your org and app credentials
npm install
npm run dev # http://localhost:4100.env.local is git-ignored; .env.example is the tracked template and holds
placeholders only:
APPENGINE_ENDPOINT=https://appengine.appmint.io
ORG_ID=your-org-id
APP_ID=your-app-id
APP_KEY=your-app-key
APP_SECRET=your-app-secretThose four values stay on the server. They are read in lib/appmint.ts through
process.env, which in Next.js is server-only unless a variable is prefixed
NEXT_PUBLIC_ — do not prefix these.
If the catalog page cannot reach AppEngine it says so on the page and points at
.env.example, rather than rendering an empty grid that looks like you have no
products.
What to take from it
The two-behaviour client. One module, honest about where it is running. It is far easier to keep credentials off the browser when the code physically cannot send them there.
An allowlist, not a pass-through. And sensitive things get their own route.
Server-rendered catalog. One round trip, real HTML, and no API surface named in the bundle.