Skip to content

API — Cart

This page documents CartController, mounted under /api/cart. The cart is free — accessible whether logged in or not (guest mode).

Controller covered

CartControllerGET /, POST /add, PUT /item/{id}, DELETE /item/{id}, DELETE /

The cart is deliberately free of access: protection chaos (Scripting chaos) applies only to the checkout journey, never to adding to the cart. This separation reflects the behavior of real e-commerce sites where adding to the cart is a "free" operation that does not warrant an anti-replay token.


Overview

Method Endpoint Auth Description
GET /api/cart None Current cart contents (items, total, count)
POST /api/cart/add None Adds a product to the cart (accumulates if already present)
PUT /api/cart/item/{id} None Updates the quantity of an item
DELETE /api/cart/item/{id} None Removes an item from the cart
DELETE /api/cart None Empties the cart entirely

Guest vs. logged-in sessions

The cart is bound to the HttpSession, not to the user. Consequences:

  • A non-logged-in user (guest) can build a cart — it is associated with their JSESSIONID.
  • On login, the CartService automatically merges the guest cart with the user's.
  • On logout, the user's cart remains attached to the session until the cookie expires (30 min by default) or until full disconnection.
flowchart LR
  G[Anonymous visitor] -->|POST /cart/add| S1[Session JSESSIONID#1<br/>guest cart]
  G -->|POST /auth/login| S2[Session with<br/>LOGGED_IN_USER]
  S1 -.merge.-> S2
  S2 -->|POST /cart/add| S2
  S2 -->|POST /auth/logout| S3[Session emptied<br/>cart lost]

GET /api/cart

Returns the full contents of the cart associated with the current session. Returns an empty cart (items: []) for an anonymous visitor with no activity.

Auth: none Controller: CartController.getCart() Service: CartService.getCartItems(session) + CartService.getCartTotal(session)

Response — 200 OK

{
  "items": [
    {
      "id": 1234,
      "productId": 42,
      "quantity": 2,
      "price": 149.99,
      "addedAt": "2026-04-08T10:30:00Z",
      "productName": "Bluetooth audio headset",
      "productImage": "/images/products/42.jpg",
      "productPrice": 149.99
    }
  ],
  "total": 299.98,
  "itemCount": 1
}
Field Description
items[] List of cart lines
items[].id CartItem identifier (DB)
items[].productId Product identifier
items[].quantity Requested quantity
items[].price Price frozen at the time of addition (useful if the product price changes)
items[].addedAt ISO timestamp of the addition
items[].productName, productImage, productPrice Product snapshot (for display)
total Sum Σ (price × quantity)
itemCount Number of distinct lines (not the total quantity)

This endpoint never returns an error under normal operation.


POST /api/cart/add

Adds a product to the cart. If the product is already present, the quantity is incremented (not replaced).

Auth: none Controller: CartController.addToCart() DTO: AddToCartRequest Service: CartService.addToCart()

Request

POST /api/cart/add HTTP/1.1
Content-Type: application/json

{
  "productId": 42,
  "quantity": 2
}
Field Type Required Constraint
productId Long yes @NotNull — existing product identifier
quantity Integer yes @NotNull, @Min(1) — at least 1

Response — 200 OK

{
  "success": true,
  "message": "Item added to cart",
  "cartItem": {
    "id": 1234,
    "productId": 42,
    "quantity": 2,
    "price": 149.99,
    "addedAt": "2026-04-08T10:30:00Z"
  }
}

The localized message comes from the cart.item.added key.

Error codes

Code Body Cause
400 {"productId": "Product ID is required"} Missing field (Jakarta validation)
400 {"quantity": "Quantity must be at least 1"} Quantity < 1
404 Product not found (propagated from CartItemNotFoundException)

PUT /api/cart/item/{id}

Updates the quantity of an existing item in the cart.

Auth: none (but session ownership is verified) Controller: CartController.updateCartItem() Service: CartService.updateCartItem() + verifyCartItemOwnership()

Parameters

Parameter Location Type Description
id path Long CartItem identifier
quantity query Integer New quantity (≥ 1)

Request

PUT /api/cart/item/1234?quantity=5 HTTP/1.1
Cookie: JSESSIONID=...

Response — 200 OK

{
  "success": true,
  "message": "Cart item updated",
  "cartItem": {
    "id": 1234,
    "productId": 42,
    "quantity": 5,
    "price": 149.99,
    "addedAt": "2026-04-08T10:30:00Z"
  }
}

Error codes

Code Body Cause Java exception
400 {"error": "Quantity must be at least 1"} quantity == null or < 1
403 {"error": "Unauthorized access to cart item"} The item does not belong to the current session CartAccessDeniedException
404 {"error": "Cart item not found: 1234"} Unknown ID CartItemNotFoundException

Business exceptions are intercepted by type in the controller, and converted to the corresponding HTTP codes. See the files com.perfshop.exception.CartItemNotFoundException and CartAccessDeniedException.


DELETE /api/cart/item/{id}

Removes an item from the cart.

Auth: none (ownership verification) Controller: CartController.removeFromCart() Service: CartService.removeFromCart()

Request

DELETE /api/cart/item/1234 HTTP/1.1
Cookie: JSESSIONID=...

Response — 200 OK

{
  "success": true,
  "message": "Item removed from cart"
}

Error codes

Same as PUT /api/cart/item/{id} (except 400 quantity):

Code Cause
403 Item belonging to another session
404 Item not found

DELETE /api/cart

Empties the cart associated with the current session entirely.

Auth: none Controller: CartController.clearCart() Service: CartService.clearCart()

Response — 200 OK

{
  "success": true,
  "message": "Cart cleared"
}

This endpoint never returns an error — even on an empty cart it responds 200 OK. The message comes from the cart.cleared key.


curl example

# Fetch the current cart (creates a session if no cookie)
curl -c /tmp/cookies.txt http://localhost:9080/api/cart

# Add 2 units of product 42
curl -b /tmp/cookies.txt \
     -H "Content-Type: application/json" \
     -d '{"productId":42,"quantity":2}' \
     http://localhost:9080/api/cart/add

# Update the quantity of item 1234
curl -b /tmp/cookies.txt -X PUT \
     "http://localhost:9080/api/cart/item/1234?quantity=5"

# Remove item 1234
curl -b /tmp/cookies.txt -X DELETE \
     http://localhost:9080/api/cart/item/1234

# Empty the cart
curl -b /tmp/cookies.txt -X DELETE http://localhost:9080/api/cart

Points of attention

The cart escapes chaos

Unlike /api/orders (checkout), no chaos applies to the cart:

  • No Scripting chaos protection
  • No falsified price injection (S5 is only active on POST /api/orders)
  • No SQL injection (S1 is only active on /api/products/search)

This is a deliberate choice: the cart is a "comfort zone" that lets the student build an order without fighting anti-replay tokens, then see chaos kick in at the critical checkout moment.

No cross-session persistence

The cart lives in the backend HTTP session — it does not survive a backend restart or cookie expiration. There is no persistent "abandoned cart" mechanism in the database for logged-in users.