A multi-location menu browser for Square sandbox merchants. It answers the same three questions the requirements describe: what the brand sells (catalog), where it can be sold (locations), and whether a guest can buy it right now at the selected storefront (availability by location, day, and local time).
The UI lets guests switch locations, browse items grouped by category, filter by category, and open an item detail view (name, description, image, correctly formatted price). All Square access stays on the server; the browser talks only to Next.js route handlers (/api/*).
- Node.js (LTS recommended) and npm
- A Square developer account and a sandbox application (Square Developer Dashboard)
npm installSecrets must never be committed. Copy the example file and fill in values:
cp .env.example .env.local| Variable | Required | Description |
|---|---|---|
SQUARE_ENVIRONMENT |
Yes | Must be sandbox as required (do not point at production merchants). |
SQUARE_ACCESS_TOKEN |
Yes | Sandbox access token from your Square application (Dashboard → Credentials). |
MENU_HIDE_OUTSIDE_AVAILABILITY |
No | If true, /api/menu omits items outside their schedule window; default keeps them with orderableNow: false. |
Optional: SQUARE_APPLICATION_ID is noted in .env.example for dashboard reference; the app does not require it for Catalog/Locations calls used here.
The requirements call for roughly two locations, several categories, several items, at least one item only at one location, and time windows for the availability bonus. This repo ships a seed script that creates “PD Challenge — Brooklyn” and “PD Challenge — Los Angeles” if missing, then upserts a catalog (breakfast windows, Brooklyn-only item, etc.).
From the project root:
npm run seedNotes:
- Re-running seed adds another catalog graph (temporary
#…ids in onebatchUpsert). Locations are idempotent by exact name. To fully reset catalog objects, delete them in the Square Sandbox Dashboard or use a fresh sandbox app (per comments inscripts/seed/index.ts). - After seeding, you can sanity-check API access:
npm run square:smokenpm run devOpen http://localhost:3000. Use the location selector and optional ?locationId= in the URL to deep-link.
npm test
npm run lint
npm run build- Next.js App Router (
src/app): UI routes and Route Handlers as a thin BFF (backend-for-frontend). src/lib/square: Square Node SDK usage (singleton client, env), listing locations and catalog.src/lib/menu: Domain logic — map Catalog objects toApi*JSON, location presence, availability windows, money formatting.src/lib/api-types.ts: Shared wire types for JSON responses (browser and server stay aligned).src/lib/client: Browser-only helpers (fetchJson,api.ts) calling/api/*— no Square token in the bundle.scripts/: CLI (seed,square:smoke) loads.env/.env.localviadotenvand imports server libs fromsrc/lib.
- Browser loads
/→src/app/page.tsxrendersMenuApp(src/components/menu-app.tsx). - Client calls
fetchLocations()→GET /api/locations→listLocations()→ Square Locations API → JSON array of{ id, name, timezone }. - User picks a location → URL updates
?locationId=→ GET/api/menu?locationId=loads Square location + full catalog →buildApiMenuResponsecomposes categories + items (presentAtLocation,orderableNow, prices, images). - User opens an item →
/item/[id]?locationId=→ GET/api/items/[id]?locationId=→buildApiItemDetail(addsdescriptionHtmlwhen present); client sanitizes HTML with DOMPurify before render.
flowchart LR
subgraph browser [Browser]
UI[React components]
Client[src/lib/client]
end
subgraph next [Next.js server]
Routes[src/app/api]
Menu[src/lib/menu]
Sq[src/lib/square]
end
Square[(Square sandbox APIs)]
UI --> Client
Client -->|same-origin /api| Routes
Routes --> Menu
Routes --> Sq
Sq --> Square
All routes return JSON. Errors use shape { "error": { "code": string, "message": string } } unless noted.
| Purpose | List merchant locations for the location switcher. |
| Query | None |
| Success | 200 — ApiLocation[] (id, name, timezone). |
| Failure | Square/SDK/network errors → 4xx/5xx with normalized code (e.g. rate limit, unauthorized). |
| Purpose | Menu for one location: categories and item summaries (price, image, presence, schedule flag). |
| Query | locationId (required) — Square catalog/location resource id. |
| Success | 200 — ApiMenuResponse: { location, categories[] } with items sorted within categories. |
| Client errors | 400 — missing or malformed locationId. |
| Not found | 404 — location id not found for this merchant token. |
| Purpose | Single item detail for the menu item id at a location. |
| Path | id — Square ITEM object id. |
| Query | locationId (required). |
| Success | 200 — ApiMenuItemDetail (summary fields + optional descriptionHtml). |
| Client errors | 400 — missing/malformed path or query ids. |
| Not found | 404 — location missing, or item not sold at location / missing priced variation. |
| Decision | Why | Trade-off |
|---|---|---|
| Next Route Handlers as BFF | Keeps SQUARE_ACCESS_TOKEN server-only; single deployment; TypeScript end-to-end. |
Extra hop vs calling Square from a separate service; fine for this scope. |
Thin handlers, fat src/lib/menu |
Easier to unit test catalog composition without HTTP; reuse from scripts/tests. | Handlers stay repetitive unless further factored. |
fetch + cache: 'no-store' on client |
Menu and prices must reflect current catalog and clock. | No HTTP caching of menu responses in the browser. |
| Primary variation only | Square allows many variations per item; menu shows one row using ordinal/name ordering. | Multi-variation choice UI not implemented. |
orderableNow + optional hide flag |
Matches requirements: off-window items visibly disabled by default; optional env to hide them for a “current menu only” demo. | Two behaviors to explain; product choice, not API ambiguity. |
| DOMPurify on item HTML | Square descriptionHtml treated as untrusted for XSS defense in depth. |
Small client bundle cost; server still returns raw HTML in JSON. |
| HTTPS-only image URLs | Avoid mixed content and odd schemes from catalog metadata. | Rare legitimate http image URLs would be dropped. |
Structured console logging |
Observable flows without adding a log vendor for this exercise. | Not structured JSON logs for production aggregation. |
The requirements note that scheduled menu availability APIs have evolved, that engineers should read Square’s docs and choose a sensible approach, and that out-of-window items should be hidden or visibly disabled using the selected location’s local timezone.
What we picked
- Square Catalog objects: categories reference
AVAILABILITY_PERIODrows (dayOfWeek,startLocalTime,endLocalTime). We resolve “now” in theLocation.timezoneIANA zone (with fallback if invalid), evaluate windows per category, and computeorderableNowon each item row. - Default UX: Items outside the window remain visible but not orderable (
orderableNow: false), with copy in the UI — aligns with “visibly disabled.” - Optional UX: Set
MENU_HIDE_OUTSIDE_AVAILABILITY=trueto omit off-window items fromGET /api/menu— aligns with “hidden” when you want a stricter “current menu only” list.
Why this approach
- It maps directly to category-linked availability periods in Catalog v2-style modeling and avoids pretending unsupported Inventory-time semantics exist for menu hours.
- Fail-safe behavior: timezone / evaluation errors do not claim an item is orderable when uncertain (
orderableNowconservative).
Details live in src/lib/menu/availability.ts, src/lib/menu/build-api-menu.ts, and seed data in scripts/seed/build-catalog.ts (e.g. breakfast category with weekday windows).
Concrete product/engineering next steps (not implementation detail):
- Search — Client-side filter over the loaded menu JSON, or server-side query param with normalized search terms; debounce; empty state when no matches.
- Modifiers — Surface Square modifier lists on item detail; enforce selection rules (min/max); reflect price deltas using variation/modifier pricing rules.
- Cart — Line items with quantity, variation/modifier snapshot, subtotal using minor-unit math (
BigInt/ explicit rounding policy); persist in memory orsessionStoragebefore payments. - Checkout / payments — Square Payments API or Orders API in sandbox with clear separation from catalog browsing; idempotency keys; never expose secrets client-side.
- Inventory / out-of-stock — Pull inventory counts or availability flags per location; merge with catalog rows; show “sold out” vs schedule-off.
- Geolocation — Browser geolocation prompt (with HTTPS + Permissions-Policy updates), suggest nearest seeded location; optional distance sort; privacy copy and graceful denial handling.
- Offline / resilience — Cache last successful menu JSON (e.g. Cache Storage or
localStoragewith TTL); stale-while-revalidate messaging; retry/backoff on failed BFF calls already partially aligned with SDK retries server-side. - Observability — Request IDs, structured JSON logs, basic metrics for
/api/*latency and Square error rates.
| Command | Purpose |
|---|---|
npm run dev |
Development server |
npm run build / npm run start |
Production build and run |
npm run seed |
Seed sandbox locations + catalog |
npm run square:smoke |
List locations + catalog count |
npm test |
Unit tests |