Swap lingering "Python/MySQL" wording for "PHP / MySQL" across the README, `src/api/` seam, the Pinia cart store, and the cart contract doc. Add endpoint specs for checkout (Stripe handoff + webhook), orders, and customers so the full plug-in surface is documented in the same style as cart.md. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
4.9 KiB
Cart API
The frontend talks to the cart through a small, stable surface exported
from src/api/cart.js. That file is the only seam that changes when the
PHP / MySQL backend comes online — everything above it (the Pinia
store, the CartDrawer component, pages that add to cart) keeps
importing the same functions with the same signatures.
Today the function bodies delegate to an in-browser Pinia store with
localStorage persistence so the UI is fully functional without a
server. Replacing the bodies with fetch() calls against the endpoints
below is a drop-in swap.
Base URL and session
- All endpoints are served under
/api. - Requests carry the session via an httpOnly cookie. The backend sets it
on first
POST, and the frontend sendscredentials: 'include'on every call. - Requests and responses are
application/json; charset=utf-8.
Endpoints
| Method | Path | Body | Returns |
|---|---|---|---|
| GET | /api/cart |
— | Cart |
| POST | /api/cart/items |
{ productId, quantity } |
Cart |
| PATCH | /api/cart/items/:productId |
{ quantity } |
Cart |
| DELETE | /api/cart/items/:productId |
— | Cart |
| DELETE | /api/cart |
— | Cart |
Every mutation returns the full, updated Cart. The client never has
to merge partial responses.
Types
type Money = number // EUR, two decimals, always > 0
type ProductId = string // /^[a-z0-9-]+$/, max 80 chars
interface ProductSummary {
id: ProductId
title: string
brand: string
size: string
image: string // absolute path, served from the frontend public/ dir
href: string // PDP URL
}
interface CartLine {
productId: ProductId
quantity: number // integer >= 1
unitPrice: Money
lineTotal: Money // unitPrice * quantity, rounded to 2dp
product: ProductSummary
}
interface Cart {
items: CartLine[]
count: number // integer, sum of all quantities
subtotal: Money // sum of all lineTotal values
updatedAt: string // ISO-8601 UTC
}
Product IDs match the id field of the catalogue exported from
src/api/products.js. The backend is the source of truth for
unitPrice and lineTotal; the client only displays what it receives.
Example exchange
Request:
POST /api/cart/items HTTP/1.1
Content-Type: application/json
{ "productId": "kaiser-natron-pulver-250-g-grosspackung", "quantity": 2 }
Response:
{
"items": [
{
"productId": "kaiser-natron-pulver-250-g-grosspackung",
"quantity": 2,
"unitPrice": 4.49,
"lineTotal": 8.98,
"product": {
"id": "kaiser-natron-pulver-250-g-grosspackung",
"title": "Kaiser-Natron® Pulver",
"brand": "Kaiser-Natron",
"size": "250 g Großpackung",
"image": "/products/kaiser-natron-pulver-250-g-gro%C3%9Fpackung.jpg",
"href": "/shop/kaiser-natron-pulver-250-g-grosspackung"
}
}
],
"count": 2,
"subtotal": 8.98,
"updatedAt": "2026-04-22T14:05:21.004Z"
}
Errors
{ "error": { "code": "product.notFound", "message": "Unknown productId." } }
Known codes:
| Code | When |
|---|---|
product.notFound |
productId is not in the catalogue. |
cart.quantityInvalid |
quantity is missing, non-integer, or < 1 on POST/PATCH. |
cart.itemLimit |
The cart exceeds its per-line or per-cart quantity cap. |
HTTP status codes:
200 OKon success (includingDELETE, which returns the updated cart).400 Bad Requestfor validation errors (cart.quantityInvalid).404 Not Foundforproduct.notFoundand forPATCH/DELETEon aproductIdthat is not in the cart.409 Conflictforcart.itemLimit.5xxfor server failures — the frontend surfaces these as a generic retry message.
Swapping the local implementation for HTTP
Replace each body in src/api/cart.js with a fetch. Example for
addToCart:
export async function addToCart(productId, quantity = 1) {
const res = await fetch('/api/cart/items', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ productId, quantity }),
})
if (!res.ok) throw await res.json().catch(() => ({ error: { code: 'network' } }))
return res.json()
}
The Pinia store in src/stores/cart.js is the frontend-side cache;
once the API is remote, hydrate it from fetchCart() on app start and
after each mutation. The CartDrawer component is state-agnostic and
keeps working either way.