Solving Under-Fetching and Over-Fetching

Let’s explore some solutions to the under-fetching and over-fetching problems.

First, let’s put more into the cart response. If each item already carries the product’s name, price, and description, the client does not need a follow-up request for each product. For a cart containing two blue mugs, the response could be:

{
  "cart_id": 73,
  "status": "open",
  "items": [
    {
      "product_id": 42,
      "name": "Blue mug",
      "unit_price_cents": 1800,
      "description": "Hand-thrown stoneware with a deep blue glaze. Holds 350 ml. Dishwasher safe.",
      "quantity": 2
    }
  ],
  "total_cents": 3600
}

For our cart page, this removes under-fetching: the cart response alone has what the page needs. It does not eliminate over-fetching, because the phone app still receives the product’s description, which it does not need.

Or we can keep the original cart response, with product identifiers and quantities, and let the client select fields in each follow-up product request. The phone app could include a fields query parameter in the request URL to ask for just the product’s name and price:

GET /products/42?fields=name,price_cents HTTP/1.1
Host: api.shop.example

This approach is called sparse fieldsets. The phone app already has the quantity the shopper wants from the cart, so it needs only the name and price from each product. The website could also request the description. Neither client needs to receive the product’s stock quantity. This reduces unused fields, but the clients still need the follow-up requests.

Used alone, each approach makes a different tradeoff:

  • Accept over-fetching and reduce under-fetching by putting product details into the cart response.
  • Accept under-fetching and reduce over-fetching by letting the client select fields.

We can also combine them: embed product details in the cart response and let each client select the fields it needs. That could solve both problems for the cart page. But we need to define how field selection works inside the items list as well as at the top level. A client might want the cart’s total, each item’s quantity, and only some of each product’s fields. Supporting those selections means defining a syntax that identifies fields at each level and rules for validating requests, including what happens when a client asks for an unknown field.

There is another option: give each frontend a dedicated backend layer that gathers data from the commerce API and shapes it for that frontend. This is the Backend for Frontend (BFF) pattern. The website’s BFF could expose GET /web/carts/73, while the phone app’s BFF could expose GET /phone/carts/73. Each would return the fields its cart page needs. The frontend makes one request to its BFF, although the BFF may still need several requests to the underlying commerce API.

For these cart pages, the BFFs can eliminate unused fields and follow-up requests from the frontends. The cost is that we now maintain backend code written for each frontend. When a page needs data its BFF does not provide, that BFF must change too. More frontends can mean more frontend-specific backend code to maintain.

Suppose we want storefront developers to be able to select fields across related resources anywhere in the API. A client could then describe the data a page needs, including fields from the cart and its products, in a single request. To support this, we need a consistent way to express those selections, define the available fields and relationships, and validate each request.