Ecommerce Boilerplate
API Reference

API Reference Overview

The public Store REST API and the authenticated Commerce RPC API at a glance.

The boilerplate exposes two distinct API families, plus a handful of smaller endpoints that back one feature each:

  • The Store REST API (/api/store/*) is a public, read-only, cache-friendly view of the catalog. It is meant for external clients — native apps, alternate frontends, integrations — that need to read products and categories without touching internal storage details.
  • The Commerce RPC API (/api/commerce/*) is the authenticated backend the app itself calls when it runs on a SQL backend (Postgres/Prisma, SQLite, or Supabase). It is an internal transport, not a public API, and every non-public method is authenticated by the configured identity provider.

On the default Firebase store the client talks to Firestore directly for shopper-side reads and writes, so most calls never reach this route. Admin mutations are the exception and always come back through it, so they can be re-authorized server-side and written to the audit trail with the server's own clock. On a SQL backend every call goes through it. See backends for how the commerce layer is wired, and audited admin writes for the Firebase exception.

The two API families

Store REST APICommerce RPC API
Base path/api/store/api/commerce/<service>/<method>
StyleRESTful, resource URLsRPC over POST
MethodGET (read-only)POST
AuthNone (public)The configured identity provider (public/user/admin)
AudienceExternal clients, native appsThe app's own client, SQL backends
CachingCDN Cache-Control headersNone — live data on every call
Versionv1Unversioned internal bridge
WritesNeverYes (carts, quotes, orders, admin)

Store REST API

The Store API reshapes data that is already publicly readable (products, categories) into a stable wire format so callers never depend on internal storage details. Inventory counts, for example, are collapsed into an inStock flag so exact stock levels are never exposed.

GET /api/store is a discovery document that lists the available endpoints and the API version:

Terminal
curl https://your-store.example.com/api/store
{
  "name": "Your Store",
  "version": "v1",
  "endpoints": [
    "/api/store/products",
    "/api/store/products/{id}",
    "/api/store/categories"
  ]
}

Key characteristics:

  • Read-only. Every endpoint is a GET. There are no public write paths.
  • Cursor pagination. GET /api/store/products returns { products, nextCursor, totalCount }; pass the returned nextCursor back as ?cursor= to page forward. See Store: Products.
  • CDN cache headers. Responses carry a public Cache-Control header so a CDN or the browser can cache them (details below).
  • Stable wire shape. Products are returned as a narrowed PublicProduct (id, productName, brand, price, categories, description, primaryImageURL, pdfLink, inStock).

Commerce RPC API

POST /api/commerce/<service>/<method> is an RPC-style bridge from the client to a server-side commerce adapter. The client posts one request per commerce call:

POST /api/commerce/products/list
Authorization: Bearer <firebase-id-token>
Content-Type: application/json

{ "args": [] }

and receives { "result": <return value> }, or { "error": string } with a 4xx/5xx status. Dates in the result serialize to ISO strings and are revived by the client.

Access levels

The caller is identified by getAuthUser(), which resolves whichever provider AUTH_PROVIDER names — a Firebase ID token by default, an Auth.js session cookie on authjs. The route never learns which; it needs a uid and nothing more. See authentication. Each whitelisted method carries one of three access levels:

LevelRequirement
publicNo auth. Catalog reads and discount lookup.
userValid token required; the method's uid argument (args[0]) must equal the token's uid, so a signed-in user can only act on their own cart/quotes/orders/profile.
adminValid token required and admin.isAdmin(uid) must return true.

quotes.submit is the one exception to the user rule: it allows a null userId for guest submissions (no token needed then), but a non-null userId must still match the token.

A method that is not on the whitelist responds 404, indistinguishable from a non-existent route. See Commerce RPC for the full whitelist and per-service argument shapes.

The Commerce RPC route runs on the Node.js runtime and requires a configured SQL backend (getServerCommerce()); if the backend is not configured, every call responds 500 with { "error": "Commerce backend is not configured." }.

Feature endpoints

The rest of /api backs one feature each. Most degrade to a no-op (or a 503) when their integration is not configured, so the default quote-based store runs without any of them.

EndpointMethodFeatureWhen unconfigured
/api/checkoutPOSTStripe Checkout (Checkout)503 { "error": "checkout not configured" } without STRIPE_SECRET_KEY
/api/checkout/configGETWhether the cart renders a Checkout button200 { "enabled": false }
/api/webhooks/stripePOSTStripe order recording (Webhooks)503 { "error": "webhook not configured" } without STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET
/api/quotes/notifyPOSTResend quote emails (Notifications)200 { "skipped": true } without RESEND_API_KEY
/api/notifications/statusPOSTResend status emails (Notifications)200 { "skipped": true } without RESEND_API_KEY
/api/newsletterPOSTMailing-list signup (Storefront)Always live. In demo mode the address is accepted and discarded — { "ok": true, "stored": false } says so
/api/auth/[...nextauth]GET/POSTAuth.js sign-in, callback, session, sign-out (Authentication)Never called unless AUTH_PROVIDER=authjs

See environment variables for the flags that switch these on.

Common error shape

Both API families return errors as a single JSON object:

{ "error": "Human-readable message" }

The Store API uses jsonError(status, message) and the Commerce RPC route uses an equivalent error(message, status) helper, so the shape is identical across the surface. Request data is never echoed back in error messages.

Standard status codes

StatusMeaning
400Bad request — malformed input (e.g. invalid cursor, unknown category, short search term, or a body that is not { args: [...] }).
401Authentication required — a protected Commerce RPC method was called without a valid token.
403Forbidden — token is valid but not permitted (acting on another user's data, or missing admin privileges).
404Not found — no such product, or an unknown Commerce RPC service.method.
429Too many requests — rate limit hit (checkout, the two notification endpoints, and newsletter signup). A Retry-After header is included.
500Internal server error — unexpected failure, or the Commerce backend is not configured.
503Service unavailable — an optional feature (e.g. Stripe checkout) is not configured.

Cache-Control policies

The Store API sets one of two Cache-Control headers depending on how quickly the underlying data changes. Both are defined in src/lib/api/store.ts:

src/lib/api/store.ts
/** Cache-Control for product endpoints (list, search, detail). */
export const PRODUCT_CACHE_CONTROL =
  "public, s-maxage=60, stale-while-revalidate=300";

/** Cache-Control for slow-changing config-backed endpoints (categories). */
export const CONFIG_CACHE_CONTROL =
  "public, s-maxage=3600, stale-while-revalidate=86400";
PolicyHeaderApplied to
PRODUCT_CACHE_CONTROLpublic, s-maxage=60, stale-while-revalidate=300/api/store/products, /api/store/products/{id} (product list, search, and detail)
CONFIG_CACHE_CONTROLpublic, s-maxage=3600, stale-while-revalidate=86400/api/store (discovery) and /api/store/categories

s-maxage is the shared-cache (CDN) freshness window in seconds; stale-while-revalidate lets a CDN serve stale content for that many additional seconds while it revalidates in the background. Products stay fresh for 60s; category and config data, which changes rarely, for an hour.

The Commerce RPC route sets no cache headers — every RPC call resolves live data. Nothing under /api is prerendered at all: route handlers have been dynamic by default since Next 15, so instead of each route restating that with a force-dynamic export, tests/api-routes-dynamic.test.ts asserts the built route table still agrees (see testing).

Explore the endpoints

On this page