Customer Module — Storefront
HTTP surface for the customer-side address book. The customer identity itself lives in Better-Auth's user table; this module owns the shopping-related customer data on top of that…
HTTP surface for the customer-side address book. The customer identity itself lives in Better-Auth's user table; this module owns the shopping-related customer data on top of that — currently just the address book.
Source:
api-modules/customer/src/controllers/store-address.controller.ts.The address book is read by the order module at place-order (for the
shippingAddress/billingAddresssnapshot) and by the cart for the address-bound shipping rate. The admin customer picker lives indocs/separated/admin/customer.md.
Conventions
Authentication
| Endpoint group | Auth |
|---|---|
GET/POST/PATCH/DELETE /store/addresses/** | required (customer session) |
Every read and write is scoped to the session's user.id. Cross-user ids return 404 Not Found, never 403 — so existence of another customer's address can't be inferred from the response code.
Response envelope
{
"data": <payload>,
"message": "Success",
"statusCode": 200,
"metadata": { /* optional, e.g. pagination */ }
}Error envelope
statusCode | errorCode examples |
|---|---|
| 400 | BAD_REQUEST, VALIDATION_ERROR |
| 401 | UNAUTHORIZED |
| 404 | NOT_FOUND |
| 500 | INTERNAL_SERVER_ERROR, DATABASE_ERROR |
Domain types
AddressResponse
type AddressResponse = {
id: string;
firstName: string;
lastName: string;
fullAddress: string; // 1..500 chars
city: string; // 1..80 chars
pincode: string; // Indian 6-digit, /^[1-9]\d{5}$/
state: string; // 1..80 chars
phone: string; // Indian, /^(\+91)?[6-9]\d{9}$/
country: string; // ISO-3166 alpha-2; default "IN"
isDefault: boolean;
createdAt: string; // ISO
updatedAt: string; // ISO
};Phone and pincode validation is India-specific today; internationalization swaps these for a country-driven validator.
Default-address rule
A customer has at most one default address. The address book invariants:
| Trigger | Behavior |
|---|---|
| First-ever address (book is empty) | Force-promoted to isDefault: true regardless of input |
Subsequent create with isDefault: true | New row becomes default; prior default flips to false |
POST /:id/default | Same — flip swap |
| Delete default | No auto-promotion — caller is left without a default until they explicitly pick one. Silent re-promotion can ship to a stale address. |
Endpoints
GET /store/addresses — List my addresses
Default address comes first; the rest are sorted by most-recently-touched.
Query
| Name | Type | Default | Constraints |
|---|---|---|---|
page | int | 1 | >= 1 |
limit | int | 20 | 1..100 |
The service converts page-based pagination to offset internally; the response uses the standard { data, metadata: { total, limit, offset, hasMore } } envelope.
Response 200 — paginated AddressResponse[].
GET /store/addresses/:id — Get one of my addresses
Response 200 — AddressResponse.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Address does not exist or belongs to another user |
POST /store/addresses — Create an address
Body
{
"firstName": "Ada",
"lastName": "Lovelace",
"fullAddress": "221B Baker Street",
"city": "London",
"pincode": "110001", // 6-digit, first digit 1-9
"state": "Delhi",
"phone": "+919876543210", // +91 prefix optional
"country": "IN", // ISO-3166 alpha-2; default "IN"
"isDefault": false
}Response 201 — AddressResponse. If this is the caller's first address, the response has isDefault: true even when the body sent false.
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_ERROR | Body fails zod (phone/pincode regex, missing required field) |
PATCH /store/addresses/:id — Update an address
Body — partial CreateAddressInput. At least one field must be present (no-op writes are rejected at the zod layer to avoid a misleading 200).
{ "city": "Mumbai", "pincode": "400001" }Response 200 — updated AddressResponse.
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_ERROR | Empty body; any field fails its individual schema |
| 404 | NOT_FOUND | Address not owned by caller |
DELETE /store/addresses/:id — Delete an address
Hard delete. Deleting the default address leaves the customer without a default until they explicitly pick one — no auto-promotion.
Response 204 No Content.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Address not owned by caller |
POST /store/addresses/:id/default — Set as default
Flips the chosen address to isDefault: true and the prior default (if any) to false. Idempotent — already-default is a no-op 200.
Response 200 — the new default AddressResponse.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Address not owned by caller |
Related modules
auth— owns theusertable that the address book references viauserId.order— snapshotsshippingAddress/billingAddressfrom this module at place-order. Seeorder.md.cart—PATCH /store/cart/addressaccepts the addressidfrom this module to pin the cart's delivery target. Seecart.md.
Catalog Module — Storefront
HTTP surface for unauthenticated catalog reads. The storefront uses these endpoints to render the navigation tree, brand / tag / ingredient pages, the product detail page (PDP),…
Dynamic Link Module — Storefront
HTTP surface for reading dynamic link groups by slug. Dynamic link groups are CMS-style ordered collections of {image, text, url} cards — used for the storefront home grid, promo…