Skip to main content

Feature: Feeding Log

Version: 1.5.0 Last Reviewed: 2026-03-15 Status: Approved

User Story

As a caregiver, I can log a feeding in less than 5 seconds so I don't lose track during sleep-deprived nights.

MVP Scope

  • Timestamp (defaults to "now", editable)
  • Type: breast, bottle, or pump (toggle)
  • Started side: left or right (breast and pump - which side feeding/pumping began on)
  • Both sides: boolean flag (did the feeding/pumping use both sides?)
  • Duration: optional number field (minutes, NOT a timer)
  • Amount: optional number field (stored as mL Float, displayed as oz for US users, bottle and pump)
  • Milk source: breast_milk or formula (bottle only, optional)
  • Breast milk type: fresh, refrigerated, frozen, colostrum, donor, antibody_rich (bottle + breast_milk only, optional)
  • Notes: optional free-text field per feeding (max 1000 chars)
  • List feedings with filtering by time range
  • Edit existing feeding
  • Delete feeding

NOT in MVP

  • Timer-based duration (user manually enters minutes after feeding completes)
  • Feeding position (lying down, sitting, etc.)
  • Configurable feeding preferences (breastfeeding-first vs bottle-first UI layout)
  • Smart time-of-day defaults
  • Feeding goals or targets
  • Reminders for next feeding
  • Milk bag lifecycle tracking (see Future: Milk Tracking below)

API Contract

Create Feeding

POST /api/v1/children/:childId/feedings
Authorization: Bearer <token>

Request (breast, single side):
{
"type": "breast",
"started_side": "left",
"both_sides": false,
"duration_minutes": 15
}

Request (breast, both sides):
{
"type": "breast",
"started_side": "left",
"both_sides": true,
"duration_minutes": 25
}

Request (bottle with milk source):
{
"type": "bottle",
"amount_ml": 120,
"duration_minutes": 12,
"milk_source": "breast_milk",
"breast_milk_type": "frozen"
}

Request (bottle with notes):
{
"type": "bottle",
"amount_ml": 90,
"notes": "Took it slowly, had some gas"
}

Request (pump, single side):
{
"type": "pump",
"started_side": "left",
"amount_ml": 80,
"duration_minutes": 20
}

Request (pump, both sides):
{
"type": "pump",
"started_side": "left",
"both_sides": true,
"amount_ml": 120
}

Response 201:
{
"feeding": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"child_id": "123e4567-e89b-12d3-a456-426614174000",
"timestamp": "2026-02-10T14:30:00.000Z",
"type": "breast",
"started_side": "left",
"both_sides": false,
"duration_minutes": 15,
"amount_ml": null,
"milk_source": null,
"breast_milk_type": null,
"notes": null,
"created_by": "789e4567-e89b-12d3-a456-426614174000",
"created_at": "2026-02-10T14:35:00.000Z",
"updated_by": "789e4567-e89b-12d3-a456-426614174000",
"updated_at": "2026-02-10T14:35:00.000Z"
}
}

Get Feeding by ID

GET /api/v1/children/:childId/feedings/:id
Authorization: Bearer <token>

Response 200:
{
"feeding": { ... }
}

List Feedings

GET /api/v1/children/:childId/feedings?since=2026-02-10T00:00:00.000Z
Authorization: Bearer <token>

Query Parameters (all optional):
- since: ISO8601 timestamp - return feedings after this time (exclusive)
- limit: integer - max number of results (default 100, max 500)

Response 200:
{
"feedings": [
{
"id": "...",
"child_id": "...",
"timestamp": "2026-02-10T15:00:00.000Z",
"type": "bottle",
"started_side": null,
"both_sides": false,
"duration_minutes": 12,
"amount_ml": 120.0,
"milk_source": "breast_milk",
"breast_milk_type": "frozen",
"notes": null,
"created_by": "...",
"created_at": "...",
"updated_by": "...",
"updated_at": "..."
}
],
"count": 1
}

Note: Results ordered by timestamp descending (most recent first).
Empty array if no feedings match the filter.

Update Feeding

PUT /api/v1/children/:childId/feedings/:id
Authorization: Bearer <token>

Request (all fields optional - partial update):
{
"timestamp": "2026-02-10T14:35:00.000Z",
"type": "bottle",
"amount_ml": 90,
"duration_minutes": 10,
"milk_source": "formula",
"notes": "Updated note"
}

Response 200:
{
"feeding": { ... }
}

Delete Feeding

DELETE /api/v1/children/:childId/feedings/:id
Authorization: Bearer <token>

Response 204: (No Content)

Error Responses

All error responses use the standard format:

{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request body validation failed",
"details": [
{ "field": "started_side", "message": "Required when type is breast", "code": "required" }
]
}
}

Error codes:

  • 400 VALIDATION_ERROR - Invalid or missing fields (with details array)
  • 401 UNAUTHORIZED - Invalid or expired token
  • 404 NOT_FOUND - Child not found, no access to child, or feeding not found (no access returns 404 to prevent child existence disclosure)

Business Rules

Validation Rules

  1. type: REQUIRED. Must be "breast", "bottle", or "pump".
  2. started_side: REQUIRED when type is "breast" or "pump". Must be "left" or "right". Ignored/stripped when type is "bottle".
  3. both_sides: Optional, defaults to false. Meaningful when type is "breast" or "pump". Ignored when type is "bottle".
  4. duration_minutes: Optional for all types. Must be a positive integer. Max 180 (3 hours).
  5. amount_ml: Optional when type is "pump" or "bottle" (pump may be omitted for comfort pumping/letdown collection). Not allowed when type is "breast". Must be a positive number (Float). Max 500 mL (~17 oz).
  6. milk_source: Optional. Must be "breast_milk" or "formula". Only valid when type is "bottle". Returns 400 if provided with type "breast" or "pump". Cleared when switching bottle→breast/pump.
  7. breast_milk_type: Optional. Must be one of "fresh", "refrigerated", "frozen", "colostrum", "donor", "antibody_rich". Only valid when type is "bottle" AND milk_source is "breast_milk". Returns 400 if provided with type "breast" or "pump", or when milk_source is "formula". Cleared when switching away from bottle type or when milk_source changes from "breast_milk".
  8. notes: Optional. Max 1000 characters. Empty string after trim is treated as null. Stored trimmed.
  9. timestamp: Optional, defaults to server's current time. Must be valid ISO8601.

Side Suggestion Logic

The app suggests which side to start on based on the last breast feeding:

  • Always suggest the opposite of the last started_side
  • This works identically for single-side and both-sides feedings
  • Example: last was started_side="left" → suggest "right"
  • Example: last was started_side="right", both_sides=true → still suggest "left"
  • If no previous breast feedings exist, suggest "left" as default
  • When user taps "Both" in the iOS quick buttons, log started_side as the suggested side, both_sides=true (assumes they started on the suggested side then switched)

Type Switching on Update

  • breast → bottle: started_side and both_sides are cleared, amount_ml and milk_source can now be set
  • bottle → breast: amount_ml, milk_source, and breast_milk_type are cleared, started_side is now required
  • breast → pump: amount_ml now allowed (optional), milk_source remains null, started_side is now required
  • pump → bottle: started_side and both_sides are cleared, milk_source can now be set
  • bottle → pump: milk_source and breast_milk_type are cleared, started_side is now required
  • pump → breast: amount_ml is cleared, milk_source remains null
  • milk_source changed from breast_milk → formula: breast_milk_type is cleared

Access Control

  • Child access verified via ChildAccess table (owner or caregiver role)
  • No access → 404 NOT_FOUND (prevents information disclosure about child existence)
  • Child not found → 404 NOT_FOUND
  • Feeding not found → 404 NOT_FOUND

Audit Trail

  • All CUD operations create an AuditLog entry
  • Create: changes = full entity
  • Update: changes = { before: {...}, after: {...} }
  • Delete: changes = full entity being deleted

Query Behavior

  • List orders by timestamp descending (most recent first)
  • "since" filters to feedings where timestamp > since value
  • Default limit 100, max 500
  • Responses wrapped in named object: { "feedings": [...], "count": N }

Acceptance Criteria

  • Can create a breast feeding with started_side and both_sides
  • Can create a bottle feeding with milk_source=breast_milk and breast_milk_type
  • breast_milk_type is rejected for breast and pump types
  • breast_milk_type is rejected when milk_source=formula
  • breast_milk_type is cleared when milk_source changes to formula
  • breast_milk_type is cleared when type changes away from bottle
  • Can create a bottle feeding with amount_ml
  • started_side is required for breast, rejected for bottle
  • amount_ml is rejected for breast, optional for bottle
  • amount_ml max is 500 mL, duration_minutes max is 180
  • Timestamp defaults to now if not provided
  • Can edit a feeding (partial update, type switching works)
  • Can delete a feeding (returns 204)
  • List returns feedings wrapped in object with count
  • List orders by timestamp descending
  • Since filter works correctly
  • Side suggestion logic: always opposite of last started_side
  • Validation errors return 400 with details array
  • No access returns 404 (same as not found, prevents existence disclosure)
  • All CUD operations create AuditLog entries

Test Cases

Create - Happy Path

  1. Breast, left side only: type=breast, started_side=left, both_sides=false → 201
  2. Breast, right side only: type=breast, started_side=right → 201 (both_sides defaults false)
  3. Breast, both sides: type=breast, started_side=left, both_sides=true → 201
  4. Bottle with amount: type=bottle, amount_ml=120 → 201, started_side=null, both_sides=false
  5. Bottle with duration: type=bottle, duration_minutes=10 → 201
  6. Custom timestamp: timestamp="2026-02-09T12:00:00.000Z" → 201, timestamp matches
  7. No timestamp: omit timestamp → 201, server sets current time
  8. Minimal breast: type=breast, started_side=left → 201 (no duration)
  9. Minimal bottle: type=bottle → 201 (no amount, no duration)

Create - Validation Errors

  1. Missing type: {} → 400, details: [{field: "type"}]
  2. Invalid type: type=formula → 400
  3. Breast without started_side: type=breast → 400, details: [{field: "started_side"}]
  4. Breast with amount_ml: type=breast, started_side=left, amount_ml=100 → 400
  5. Negative duration: duration_minutes=-5 → 400
  6. Duration too high: duration_minutes=200 → 400 (max 180)
  7. Amount too high: type=bottle, amount_ml=600 → 400 (max 500)
  8. Invalid started_side: started_side=middle → 400
  9. Bottle with started_side: type=bottle, started_side=left → 201, started_side stripped to null
  10. Invalid timestamp: timestamp="not-a-date" → 400

Access Control

  1. No child access: user without ChildAccess → 404 (prevents child existence disclosure)
  2. Caregiver access: user with caregiver role → 201
  3. Non-existent child: invalid childId → 404
  4. No auth token: missing Authorization header → 401
  5. Expired token: expired session → 401

List

  1. List all: GET feedings → 200, { feedings: [...], count: N }
  2. List with since: feedings at T1, T2, T3; since=T1.5 → returns T2 and T3
  3. List empty: child with no feedings → 200, { feedings: [], count: 0 }
  4. List ordering: feedings at T2, T1, T3 → returned as [T3, T2, T1]
  5. List with limit: 5 feedings, limit=2 → returns 2 most recent
  6. List no access: → 404
  7. List non-existent child: → 404

Update

  1. Update timestamp: new timestamp → 200
  2. Update duration: duration_minutes=20 → 200
  3. Breast to bottle: type=bottle, amount_ml=100 → 200, started_side cleared
  4. Bottle to breast: type=breast, started_side=left → 200, amount_ml cleared
  5. Bottle to breast without side: type=breast (no started_side) → 400
  6. Update started_side: started_side=right → 200
  7. Update both_sides: both_sides=true → 200
  8. Partial update: only duration_minutes → 200, other fields unchanged
  9. Update non-existent: invalid id → 404
  10. Update no access: → 404

Delete

  1. Delete feeding: → 204
  2. Delete then list: feeding not in results
  3. Delete non-existent: → 404
  4. Delete no access: → 404

Audit Log

  1. Create audit: create feeding → AuditLog with action=create
  2. Update audit: update feeding → AuditLog with action=update, changes={before, after}
  3. Delete audit: delete feeding → AuditLog with action=delete

Side Suggestion (business logic, tested via dashboard API)

  1. Alternation: last started_side=left → suggest right
  2. Alternation after both: last started_side=right, both_sides=true → suggest left
  3. No history: no breast feedings → suggest left
  4. Bottle doesn't affect suggestion: last feed was bottle, before that breast started_side=left → suggest right (skip bottles)
  5. Multiple children isolated: Child A feedings don't affect Child B suggestions

breast_milk_type

  1. Bottle + breast_milk + type: type=bottle, milk_source=breast_milk, breast_milk_type=frozen → 201, field saved
  2. Bottle + formula + type: type=bottle, milk_source=formula, breast_milk_type=frozen → 400, field rejected
  3. Breast + type: type=breast, breast_milk_type=fresh → 400, field rejected
  4. Update clears on milk_source change: bottle+breast_milk+frozen, update milk_source=formula → breast_milk_type=null
  5. Update clears on type change: bottle+breast_milk+fresh, update type=breast → breast_milk_type=null

Edge Cases

  1. All optional fields present: breast with started_side, both_sides, duration_minutes, timestamp → 201
  2. Future timestamp: timestamp 1 hour ahead → 201 (allowed)
  3. Very old timestamp: timestamp 2 years ago → 201 (allowed for historical data)
  4. Amount_ml decimal: type=bottle, amount_ml=73.9 → 201 (Float stored precisely)
  5. Concurrent updates: two users update same feeding → last write wins, both create AuditLog

Boundaries

  • No timer-based duration tracking (client-side concern)
  • No formula vs pumped breastmilk distinction for bottle type (milk_source distinguishes these; breast_milk_type further classifies breast milk)
  • No feeding position, location, or environmental notes
  • No multi-feeding sessions (breast then bottle top-up as single event)
  • Unit conversion (mL ↔ oz) is a client responsibility. API always stores mL.
  • Side suggestion logic lives in the dashboard data API, not in the feeding endpoints themselves
  • Delete returns 204 No Content (no body)

Future: Milk Tracking (Out of Scope)

Post-MVP feature: dedicated pumping log with milk bag lifecycle tracking.

  • Each pumped bag gets a unique ID + timestamp
  • States: fresh → frozen → thawed → used/discarded
  • Usage linking: "Bottle feeding X used milk bag Y"
  • Metadata: baby health status, mom health status, medications, alcohol
  • Nutritional context: sick baby, sick mom, etc.
  • This will be a separate entity/feature, not an extension of Feeding.