Bugnik
← All problems

31Gift Registry API

Node.js · ExpressEasyBug fixSuggested time: 60 min

Gift registry REST API (Express + MongoDB; grace window, item counts, owner-only actions)

New to repository-based OA questions? Read the guide.

Sign in to start

Before you start

You'll get more out of this problem if you're comfortable with:

Language

  • JavaScript objects and arrays
  • CommonJS modules: require and module.exports

Libraries and frameworks

  • Express 5 routers, middleware and an error-handling middleware
  • Mongoose models with embedded sub-documents

Build and test tools

  • Reading Node.js startup errors and stack traces
  • Reading Jest + Supertest failures (expected vs received)

Reasoning

  • Dates as YYYY-MM-DD strings

Background

The Celebrations team at a large online retailer runs the gift registry service: a couple (or anyone planning an event) creates a registry, adds the products they'd like, and shares it; guests buy from it and the owner sees who bought what. The service is a Node.js REST API (Express + MongoDB via Mongoose). A recent refactor shipped with several defects.

How the system works

Callers

  • Every request except GET /registries/:id needs an x-user-id header (the caller's id, already authenticated upstream). A missing or blank header is 401 with { "error": "sign in required" }.
  • The registry's owner is the user who created it.

Errors

Every error response is { "error": "<message>" }:

  • 400 invalid input, or an :id that isn't a valid id
  • 401 no x-user-id
  • 403 the caller isn't allowed (see each endpoint)
  • 404 the registry, or the item, doesn't exist
  • 409 the request breaks a rule (duplicate item, closed registry, not enough units left)

Dates

  • Event dates are calendar dates written YYYY-MM-DD (a real date: 2026-02-29 is invalid).
  • "Today" is the UTC date of the service clock.

The registry object

Every endpoint that returns a registry returns:

{
  "id": "665f…",
  "ownerId": "owner-ana",
  "title": "Ana and Sam's wedding",
  "eventDate": "2026-06-10",
  "status": "OPEN",
  "acceptingPurchases": true,
  "items": [
    { "sku": "MUG-01", "name": "Stoneware mug", "priceCents": 1850, "wanted": 2, "purchased": 1, "remaining": 1 }
  ]
}
  • status is OPEN or CLOSED. Items are listed in the order they were added.
  • remaining = wanted − purchased.
  • acceptingPurchases is true when the registry is OPEN and today is on or before the 7th day after the event date, inclusive. Event on June 10: purchases are accepted through June 17 and refused from June 18.

Endpoints

POST /registries (signed in): body { "title", "eventDate" }.

  • title is trimmed and must be 1–80 characters.
  • eventDate must be a valid date, today or later.
  • Returns 201 with the new registry (OPEN, no items).

GET /registries/:id (no sign-in needed): 200 with the registry, 404 if there is none.

POST /registries/:id/items (owner only, else 403): body { "sku", "name", "priceCents", "wanted" }.

  • sku is trimmed and upper-cased, then must be 3–20 characters of A–Z, 0–9 or -.
  • name is trimmed and must be 1–60 characters.
  • priceCents must be a whole number ≥ 1.
  • wanted is optional: when it is missing (absent or null) it is 1. Otherwise it must be a whole number from 1 to 20; 0, negatives, fractions and strings are 400.
  • A SKU that is already on the registry is 409. A CLOSED registry can't get new items (409).
  • Returns 201 with the updated registry.

POST /registries/:id/close (owner only, else 403): sets status to CLOSED and returns 200 with the registry. Closing a closed registry is fine (same response).

POST /registries/:id/purchases (signed in): body { "sku", "qty", "giftMessage" }.

  • Checks, in this order:
    1. the registry exists (404)
    2. the input is valid (400): qty is optional and is 1 when missing (absent or null), otherwise a whole number ≥ 1 (0, negatives and fractions are invalid); giftMessage is optional text of at most 200 characters
    3. the caller isn't the owner (403)
    4. the registry is accepting purchases (409)
    5. the item exists; sku is matched trimmed and upper-cased (404)
    6. qty is at most the item's remaining (409 with "only N remaining", for example "only 1 remaining")
  • Returns 201 with { "purchase": { "id", "registryId", "sku", "qty", "buyerId", "giftMessage", "createdAt" }, "remaining": <units left> }, where createdAt is the service clock as an ISO timestamp.

GET /registries/:id/purchases (owner only, else 403): 200 with { "purchases": [...] }, newest first.

GET /registries/:id/summary (owner only, else 403): 200 with { "itemCount", "unitsWanted", "unitsPurchased", "fullyPurchased", "spentCents" }:

  • itemCount: items on the registry; unitsWanted / unitsPurchased: the sums of wanted / purchased
  • fullyPurchased: items with nothing remaining
  • spentCents: the sum of priceCents × purchased over all items

Project structure

src/
  app.js                         createApp(): JSON parsing, routes, error handler
  server.js                      connects to MongoDB and starts the server
  routes/registries.js           URL → middleware → controller
  controllers/
    registryController.js        create, get, add item, close, summary
    purchaseController.js        buy an item, list purchases
  middleware/
    requireUser.js               x-user-id → req.userId, else 401
    errorHandler.js              errors → status + { error }
  models/
    Registry.js                  registry with embedded items
    Purchase.js                  one purchase
  services/
    registryService.js           lookups, ownership, the purchase window, items, closing
    purchaseService.js           purchase rules and listing
    summaryService.js            the owner's totals
  utils/
    AppError.js                  an error with an HTTP status
    dates.js                     YYYY-MM-DD validation and date math
    input.js                     reading counts and text from request bodies
    present.js                   documents → API JSON
tests/
  helpers.test.js                dates and input helpers
  registries.test.js             registry endpoints
  purchases.test.js              purchase endpoints
  setup/                         in-memory MongoDB for the tests

Your task

Guests and the QA team have reported problems since the refactor:

  • The service no longer starts.
  • A couple says guests couldn't buy anything a week after their wedding, although the registry should still be open.
  • Adding or buying zero units of an item goes through as if one unit were asked for.

Find and fix the defects so that all tests pass.

  • Do not modify tests/, package.json or jest.config.js.
  • Keep your changes minimal. Fix the defects; don't rewrite modules.
  • Additional hidden tests check the behaviour described in this README.