Exact wire contract for every screen → endpoint. Base /api/v1; envelope per
00-shared/07 . All endpoints from src/modules/parents/controllers/parent.controller.ts;
business rules from parent.service.ts and student-parent-link.service.ts.
Global guards: RateLimitGuard → JwtAuthGuard → RbacGuard (app.module.ts:129-131);
no endpoint carries @Permissions metadata — parent endpoints are effectively
JWT-only (OQ-8).
Aspect Contract
Base https://api.<domain>/api/v1
Headers Authorization: Bearer <accessToken>; x-request-id client-generated; Content-Type: application/json
success {success:true, message:"OK", data, meta?, timestamp, requestId} (response-envelope.interceptor.ts:47-60)
error {success:false, message, error:{code, details?}, timestamp, requestId} (http-exception.filter.ts:73-81)
Codes 400 VALIDATION_ERROR / 401 UNAUTHENTICATED / 403 PERMISSION_DENIED / 404 RESOURCE_NOT_FOUND / 409 DUPLICATE_RESOURCE / 422 BUSINESS_RULE_VIOLATION / 429 RATE_LIMITED / 5xx INTERNAL_SERVER_ERROR (http-exception.filter.ts:27-35)
Tenancy tenantId from JWT only (jwt-auth.guard.ts:44-55); injected by repository (base.repository.ts:33-35) — never in body
Rate tier api 100/min (global RateLimitGuard, prod-only)
Caching none server-side for parents; client last-good cache (list)
Offline reads cached; writes blocked
Retry backoff on 5xx/network; no auto-retry on 429
Endpoint GET /parents
Query page (1-based), limit (1–100, default 20), sort, q — only page/limit honored (parent.service.ts:58-61; pagination-query.dto.ts:5-30)
Success 200 data: ParentDocument[], meta:{page,limit,totalItems,totalPages,hasNext,hasPrevious}
Parent doc shape {_id, tenantId, userId, occupation?, company?, annualIncome?, relationshipNotes?, emergencyContactPriority, pickupAuthorization, metadata?, createdAt, updatedAt, createdBy?, updatedBy?, version} (parent.schema.ts:8-32, base.schema.ts:8-35)
Client flow page 1 → infinite scroll while meta.hasNext; RefreshIndicator reset
Errors 400 bad page/limit; 401; 429; 5xx
Endpoint GET /parents/:id → 200 data: ParentDocument; 404 RESOURCE_NOT_FOUND "Parent not found." (parent.service.ts:49-53)
Endpoint GET /parents/:id/students → 200 data: StudentParentLinkDocument[] (raw link docs , parent.service.ts:69-72)
Link doc shape {_id, tenantId, studentId, parentId, relationship, isPrimaryGuardian, financialResponsibility, pickupAllowed, emergencyPriority, metadata?, createdAt, updatedAt, version} (student-parent-link.schema.ts:16-41)
Join client fetches GET /students/:id per studentId (OQ-10)
Errors 404 (parent); 400 invalid id (CastError → "Invalid resource identifier.", http-exception.filter.ts:48,92)
Endpoint POST /parents body CreateParentDto (create-parent.dto.ts)
Required userId (@IsMongoId)
Optional occupation, company, annualIncome, relationshipNotes, emergencyContactPriority, pickupAuthorization
Success 200/201 data: ParentDocument (Nest default 201 for @Post; envelope interceptor doesn't alter status — verify in e2e)
Side effect ParentCreated event → in-app queue job parent-created (parent.service.ts:38-45, event-queue-map.ts:37)
Errors 400 validation; 409 DUPLICATE_RESOURCE "Parent profile already exists for this user." (parent.service.ts:32-34); 429; 5xx
Endpoint PATCH /parents/:id body UpdateParentDto (update-parent.dto.ts) — partial $set; userId not accepted
Success 200 data: ParentDocument (updated; version incremented, base.repository.ts:62-65)
Side effect ParentUpdated → audit-write log-parent-updated (parent.service.ts:78-86, event-queue-map.ts:38)
Errors 400; 404 "Parent not found." (parent.service.ts:77); 429; 5xx
Endpoint DELETE /parents/:id
Success 200 data: undefined (void handler, parent.service.ts:89-100)
Side effect ParentDeleted → audit-write log-parent-deleted (event-queue-map.ts:39); links are NOT touched (OQ-3)
Errors 404 "Parent not found." (parent.service.ts:91)
Endpoint POST /parents/link/:studentId body LinkParentDto (link-parent.dto.ts) — studentId in the URL , not body
Required parentId (@IsMongoId), relationship (string — enum not validated in DTO , OQ-6)
Optional isPrimaryGuardian, financialResponsibility, pickupAllowed (default true), emergencyPriority
Success 200/201 data: StudentParentLinkDocument
Validation reality studentRepo.findById(studentId) result discarded — missing student does not 404 (OQ-3); duplicates allowed (OQ-2)
Errors 400 (bad ids/fields); 429; 5xx (invalid relationship → Mongoose ValidationError → 500, OQ-6)
Endpoint GET /parents/link/student/:studentId → 200 data: StudentParentLinkDocument[] (student-parent-link.service.ts:34-36)
Join client fetches GET /parents/:id per parentId
Endpoint DELETE /parents/link/:linkId
Success 200 data: undefined (void, student-parent-link.service.ts:38-41) — soft delete
Errors 404 RESOURCE_NOT_FOUND "Link not found." (student-parent-link.service.ts:40)
Screen Loading Streaming Realtime
parents list AppSkeleton(list)— —
parent detail header skeleton + children skeletons children load after header —
create/edit/link CTA spinner — —
my children (forward-looking) skeleton — (planned) WS parent.linked
Screen code UI
create 409 banner + "Open existing profile"
any form 400 per-field details
detail/edit 404 AppErrorState + back
unlink 404 treat-as-removed
link 500 (invalid relationship) generic + requestId (client prevents via dropdown)
any 401 silent refresh → session expiry
any 429 countdown
any 5xx generic + requestId + retry
GET /parents is the only paginated endpoint (meta as above). Link lists
(GET /parents/:id/students, GET /parents/link/student/:studentId) are
non-paginated full arrays (student-parent-link.repository.ts:20-26) — client
renders all; per-student join is bounded by household size.
No optimistic writes on any parents mutation — link/unlink are consequential;
server-confirm everywhere (00-shared/06 §3.5 ).
Undo: none (no server endpoint reverses an unlink; soft-deleted links are not
restorable via API).
Read lists: RefreshIndicator always bypasses client cache.