docs
/
Client Integration

Server proxy

The pattern that puts your own server between the browser and AppEngine, with the working implementation from base-app.

The browser calls your origin. Your server calls AppEngine. This page is the mechanics of that middle layer, taken from base-app, which does exactly this for every AppMint-hosted storefront.

A single catch-all route

One route handler forwards everything, rather than a hand-written endpoint per resource. In Next.js that is src/app/api/[...slug]/route.ts:

export async function GET(request: NextRequest, { params }) {
  return handleRequest('GET', request, await params);
}
export async function POST(request: NextRequest, { params }) {
  return handleRequest('POST', request, await params);
}
// … PUT, PATCH, DELETE

So GET /api/storefront/products?ps=20 in the browser becomes GET storefront/products?ps=20 against AppEngine, with credentials attached server-side.

The handler does four things: rebuild the path, read the body, assemble identity, delegate.

async function handleRequest(method: string, request: NextRequest, params: { slug: string[] }) {
  // 1. Path and query, preserved including repeated keys
  let clientPath = params.slug.join('/');
  const url = new URL(request.url);
  clientPath = clientPath + url.search;

  // 2. Body, by content type
  let clientData = null;
  if (['POST', 'PUT', 'PATCH'].includes(method)) {
    const contentType = request.headers.get('content-type');
    if (contentType?.includes('application/json')) clientData = await request.json();
    else if (contentType?.includes('multipart/form-data')) clientData = await request.formData();
    else clientData = await request.text();
  }

  // 3. Identity — the visitor's token, plus telemetry built from headers
  const authHeader = request.headers.get('authorization');
  const { token } = await getAppmintAuth().getClientInfo();
  const clientAuthorization = authHeader || token;

  const clientInfo: any = (await getRequestInfo(request.headers)) || {};
  if (clientAuthorization) clientInfo.authorization = clientAuthorization;

  // 4. Delegate — the client owns app auth, orgid and retry
  const result = await getAppEngineClient().processRequest(
    method, clientPath, clientData, clientAuthorization, clientQuery, clientInfo, isMultiPath,
  );
  return NextResponse.json(result);
}

Note what the browser never sees: the application token, the orgid resolution, and the AppEngine host itself.

Preserve the upstream status

The most common mistake is collapsing every failure into a 500. Forward what AppEngine said:

catch (error) {
  const axiosError = error as any;
  if (axiosError.response) {
    return NextResponse.json(
      { error: axiosError.response.data || axiosError.message, status: axiosError.response.status },
      { status: axiosError.response.status },
    );
  }
  return NextResponse.json({ error: error.message }, { status: 500 });
}

A 404 that arrives as a 500 turns a missing record into an outage, and a 422 that arrives as a 500 hides a validation message the user needed to read.

This is a forwarder, not an authoriser

The catch-all is deliberately thin, and that has a consequence worth stating plainly: anything reachable on AppEngine is reachable through it. It moves credentials off the browser. It does not, by itself, decide what a visitor may do.

Where a visitor could ask for something they should not, add the check in your own route before delegating — or give that resource its own route rather than letting it fall through the catch-all.

Do not expose administrative or repository-level endpoints through a public catch-all. Keep the proxy to the client/* and storefront surfaces a visitor legitimately needs — in particular, forward client/content-studio/* but never the staff content-studio/* routes.

Server-rendered pages skip the proxy

The proxy exists for the browser. Code already running on your server should call AppEngine directly — going out through your own HTTP route to come back in is a wasted round trip.

// In a server component / route handler
const site = await getAppEngineClient().getSite(hostName);
const { page } = await getPageData(slug, headersList);

Worked example: pricing a cart

The browser posts the cart to your origin; your server prices it; the response is rendered verbatim.

// Browser
const res = await fetch('/api/storefront/pricing/calculate-cart', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ productItems, rentalItems, couponCode, shippingAddress }),
});
const summary = (await res.json()).data ?? await res.json();
{
  "subtotal": 900,
  "discount": 0,
  "tax": 74.25,
  "productShipping": 75,
  "total": 1049.25,
  "shippingMethod": "flat",
  "freeShipping": false,
  "valid": true
}

Render total as given — 1049.25. Adding subtotal + productShipping yields 975 and silently loses the tax.

Send couponCode and shippingAddress with the cart, not as separate calls. The server nets the discount off and resolves shipping in the same pass; asking separately produces a summary that disagrees with itself.

An unknown code is reported rather than thrown, and leaves the money untouched:

{ "valid": false, "reason": "not_found", "message": "Discount code not found", "total": 1049.25 }