Flat Cookbook List API

Table of Contents

1. Context

V3 cookbook list endpoint currently returns deeply nested data — sections (containing items with pairing_id~/~product_id), a full author object, and collections objects. CookbookSection and CookbookCollection data models are deprecated. Frontend only needs flat ID lists from the list view.

Goal: remove nesting from the list endpoint. Detail endpoint and underlying data models stay unchanged.

2. Response Shape Change

2.1. Before

{
  "id": 1,
  "slug": "...",
  "author": {"id": 42, "name": "...", "slug": "...", ...},
  "collections": [{"id": 1, "name": "...", ...}],
  "sections": [
    {
      "id": 10,
      "name": "...",
      "position": 0,
      "items": [
        {"id": "pairing-101", "item_type": "pairing", "pairing_id": 101, "product_id": null},
        {"id": "product-201", "item_type": "product", "pairing_id": null, "product_id": 201}
      ]
    }
  ],
  "pairing_count": 1,
  "product_count": 1
}

2.2. After

2.2.1. Public/Thematic cookbook:

{
  "id": 1,
  "slug": "summer-grilling",
  "name": "Summer Grilling",
  "description": "Great recipes for the grill.",
  "hero_image": "https://cdn.example.com/images/summer-grilling.jpg",
  "is_featured": false,
  "is_active": true,
  "sort_order": 0,
  "cookbook_type": "THEMATIC",
  "visibility": "SHARED",
  "owner_id": null,
  "author_id": 42,
  "dietary_tag_ids": [3, 7],
  "pairing_ids": [101, 102],
  "product_ids": [201],
  "pairing_count": 2,
  "product_count": 1,
  "referral_code": "",
  "count": 50,
  "next": "https://api.example.com/api/v3/cookbooks/?page=2",
  "previous": null
}

2.2.2. Shared/Favorites cookbook:

{
  "id": 1,
  "slug": "xjK32jal",
  "name": "Colin's Favorites",
  "description": "",
  "hero_image": "https://cdn.example.com/images/summer-grilling.jpg",
  "is_featured": false,
  "is_active": true,
  "sort_order": 0,
  "cookbook_type": "FAVORITES",
  "visibility": "SHARED",
  "owner_id": null,
  "author_id": 1402942,
  "dietary_tag_ids": [3, 7],
  "pairing_ids": [101, 102],
  "product_ids": [201],
  "pairing_count": 2,
  "product_count": 1,
  "referral_code": "customer-referral-code",
  "count": 50,
  "next": "https://api.example.com/api/v3/cookbooks/?page=2",
  "previous": null
}

Fields removed: author (nested), sections (nested), collections (nested). Fields added: author_id (nullable int), pairing_ids (flat list), product_ids (flat list).

3. Implementation Plan

3.1. 1. New serializer — cookbooks/serializers.py

Add CookbookListSerializer after existing CookbookSerializer. List view gets new serializer; detail view keeps CookbookSerializer unchanged.

class CookbookListSerializer(serializers.Serializer):
    # all scalar fields from CookbookSerializer
    # (id, slug, name, description, hero_image, is_featured, is_active,
    #  sort_order, cookbook_type, visibility, kind, owner_id,
    #  dietary_tag_ids, pairing_count, product_count, referral_code,
    #  count, next, previous)
    author_id = serializers.IntegerField(source="author.id", allow_null=True, default=None)
    pairing_ids = serializers.SerializerMethodField()
    product_ids = serializers.SerializerMethodField()

    def get_pairing_ids(self, obj) -> list[int]:
        return [
            item.pairing_id
            for section in obj.sections.all()
            for item in section.items.all()
            if item.pairing_id is not None
        ]

    def get_product_ids(self, obj) -> list[int]:
        return [
            item.product_id
            for section in obj.sections.all()
            for item in section.items.all()
            if item.product_id is not None
        ]

Uses already-prefetched sections__items — no extra queries.

3.2. 2. Wire up list view — cookbooks/views.py

In CookbookListView:

  • serializer_class = CookbookListSerializer (was CookbookSerializer)
  • Drop collections from prefetch_related in the list queryset (no longer serialized).
  • Keep sections__items prefetch (still needed for pairing_ids~/~product_ids).

3.3. 3. Update tests — cookbooks/tests/test_api.py

Update CookbookV3ReadAPITestCase list tests to assert new shape:

  • sections key absent
  • author_id present (int or null), author key absent
  • collections key absent
  • pairing_ids is a flat list of ints
  • product_ids is a flat list of ints
  • Existing filter/count tests that don’t inspect shape still pass

4. Verification

python manage.py test cookbooks.tests.test_api.CookbookV3ReadAPITestCase

Manual check:

curl http://localhost:8000/api/v3/cookbooks/ | python -m json.tool | grep -E 'author|section|collection|pairing_ids|product_ids'

Expect: author_id, pairing_ids, product_ids present; author, sections, collections absent.

Date: 2026-06-12

Author: Colin Powell

Created: 2026-06-12 Fri 12:30