Exact wire contract for every screen → endpoint. Base /api/v1; envelope per
00-shared/07 . All endpoints from src/modules/library/library.controller.ts
(@Controller('books'), :23); business rules from library.service.ts.
Guards: @UseGuards(JwtAuthGuard) at controller level (library.controller.ts:22)
— no RBAC metadata (OQ-1). Tenant from JWT only; never in body
(base.repository.ts scoped filters).
Aspect Contract
Base https://api.<domain>/api/v1
Headers Authorization: Bearer <accessToken>; x-request-id; Content-Type: application/json
Response {success, message:"OK", data, meta?, timestamp, requestId} (response-envelope.interceptor.ts:44-52)
Error {success:false, message, error:{code, details?}, timestamp, requestId} (http-exception.filter.ts:17-24,74-78); codes include VALIDATION_ERROR (400), DUPLICATE_RESOURCE (409), RATE_LIMITED (429), RESOURCE_NOT_FOUND (404) (:28-34)
Pagination GET /books only: page (≥1, default 1), limit (default 20) (library.controller.ts:36-38); meta = {page, limit, totalItems, totalPages, hasNext, hasPrevious} via buildPaginationMeta (library.service.ts:75); borrow endpoints are not paginated (library.service.ts:195-201)
Caching reads cached client-side (catalog 5 min, borrows 1 min, detail 1 min); no cache on write responses
Offline reads from cache; writes blocked (no offline queue)
Retry backoff on 5xx/network; no auto-retry on 409 or 429
Idempotency none server-side (issue/return are not idempotent; double-tap guarded client-side — QA-2)
Endpoint GET /api/v1/books?page&limit&q (library.controller.ts:33-41)
Query page (default 1), limit (default 20) — Number() cast (:40); q optional
Success 200 data: [BookDoc…] + meta; with q → regex title/author/ISBN case-insensitive (book.repository.ts:26-30); without → all, sort: {title: 1} (:36); skip (page−1)*limit (library.service.ts:59,71)
Count matched via $or regex or {} (library.service.ts:60-67,73)
Errors 400 invalid ints; 5xx
Filters category/status/availability not supported server-side — client-side only (OQ-5)
BookDoc (book.schema.ts:15-48, base.schema.ts):
_id, tenantId, title, author, isbn, publisher?, category?, edition?, totalCopies, availableCopies, status(available|borrowed|damaged|lost), shelfLocation?, description?, createdAt, updatedAt, version. Indexes: {tenantId,isbn} unique
(:51), {tenantId,title} (:52), {tenantId,author} (:53).
Endpoint GET /api/v1/books/:id (library.controller.ts:43-47)
Success 200 data: BookDoc
Errors 404 RESOURCE_NOT_FOUND "Book not found." (library.service.ts:80); 400 invalid id (CastError → VALIDATION_ERROR)
Endpoint POST /api/v1/books (library.controller.ts:27-31)
Body CreateBookDto (create-book.dto.ts:4-47): title*, author*, isbn*, publisher?, category?, edition?, totalCopies?(@Min 1), shelfLocation?, description?
Success 201 data: BookDoc — totalCopies = dto.totalCopies ?? 1, availableCopies = totalCopies (library.service.ts:41-46)
Errors 409 DUPLICATE_RESOURCE "Book with ISBN "…" already exists." (library.service.ts:37-39) + unique index {tenantId,isbn} (book.schema.ts:51); 400 validation
Endpoint PATCH /api/v1/books/:id (library.controller.ts:49-53)
Body any subset of CreateBookDto fields (update-book.dto.ts:4)
Success 200 data: BookDoc; if totalCopies changed → availableCopies = max(0, available + Δ) (library.service.ts:87-90)
Errors 404 (library.service.ts:92); 400 validation; 409 duplicate ISBN if ISBN changed to an existing one
Endpoint DELETE /api/v1/books/:id (library.controller.ts:55-59)
Success 200 — soft delete (library.service.ts:103 via BaseRepository.softDelete)
Errors 409 "Cannot delete book with active borrow records." (library.service.ts:99-101); 404 (:104)
Endpoint POST /api/v1/books/issue (library.controller.ts:61-65)
Body {bookId*, studentId*, dueDate* (ISO), notes?} (issue-book.dto.ts:4-21)
Success 201 data: BorrowRecordDoc — status: ACTIVE, borrowedAt: now, dueDate as sent (library.service.ts:132-139); book availableCopies − 1, status → borrowed if 0 remain (:122-130)
Side effect BookIssued {bookId, studentId} domain event (library.service.ts:141-148)
Errors 404 book (:108); 409 "No copies available for borrowing." (:109-111); 409 "Student already has maximum number of borrowed books." (cap 5, :116-120); 400 validation
Note no server min-date on dueDate — client enforces future; no idempotency key
Endpoint POST /api/v1/books/return (library.controller.ts:67-71)
Body {borrowRecordId*, fineAmount?(@Min 0), notes?} (return-book.dto.ts:4-19)
Success 200 data: BorrowRecordDoc — status: RETURNED, returnedAt: now, fineAmount = dto.fineAmount ?? calculateFine(dueDate), fineStatus = pending iff fineAmount > 0 else unset (library.service.ts:159-169); book availableCopies + 1, status → available (:172-178)
Side effect BookReturned {bookId, studentId, fineAmount} event (library.service.ts:180-191)
Errors 404 record (:153-154); 409 "Book was not actively borrowed." (:155-157); 400 validation
Note fineAmount override is trusted server-side — staff-only UI (QA-4)
BorrowRecordDoc (borrow-record.schema.ts:20-58):
_id, tenantId, bookId (populated by repos: borrow-record.repository.ts:31,42), studentId, issuedBy? (never written — OQ-3), borrowedAt, dueDate, returnedAt?, status(active|returned|overdue|lost), fineAmount, fineStatus?(pending|paid|waived), notes?, createdAt, updatedAt. Indexes (:61-63): {tenantId,bookId,studentId},
{tenantId,studentId,status}, {tenantId,dueDate,status}.
History GET /api/v1/books/borrows/:studentId (library.controller.ts:73-77) → 200 [BorrowRecordDoc…] sorted borrowedAt desc (library.service.ts:195-197); no pagination, no populate of studentId
Active GET /api/v1/books/borrows/:studentId/active (library.controller.ts:79-83) → 200 [BorrowRecordDoc…] where status: ACTIVE (library.service.ts:199-201, borrow-record.repository.ts:24-32, bookId populated :31)
Errors 400 invalid id; 5xx
Note no me variant — client supplies studentId from profile (OQ-9)
Endpoint POST /api/v1/books/fines/:borrowRecordId/pay (library.controller.ts:85-89) — no body
Success 200 data: BorrowRecordDoc with fineStatus: PAID (library.service.ts:203-209)
Errors 404 record (:207); 400 invalid id
Note server does not check fineStatus before paying and does not validate fine > 0 (OQ-6) — client gates the button
Need Status
List all active borrows (overdue sweep) (planned) — findOverdue() exists unused (borrow-record.repository.ts:34-43); IMPLEMENTATION_PLAN.md:228
Mark book damaged/lost, record lost (planned) — enums exist (book.schema.ts:7-12)
Renew / reserve (planned) — IMPLEMENTATION_PLAN.md:228
Catalog import/export (planned) — IMPLEMENTATION_PLAN.md:172,228
Waive fine (planned) — FineStatus.WAIVED exists (borrow-record.schema.ts:17) but nothing sets it
Book cover / attachments (proposed) — files module exists (files.controller.ts:29-71), Book has no cover field (OQ-8)
QR/barcode scanning (forward-looking)
Screen Loading Realtime
Catalog list AppSkeletonre-fetch on focus; WS (planned)
Book detail skeleton re-fetch on focus (issue/return elsewhere)
Borrowed-by-me skeleton re-fetch on focus
Issue/Return/Fine pay button spinner —
Screen code UI
issue 409 (copies/cap) banner + refresh availability
return 409 (not active) banner "Already returned" + refresh
create/update 409 (ISBN) inline field error
any 404 AppEmptyState / close form + snackbar
any 400 field errors
any 401 → refresh → fail sessionExpired
any 429 countdown, no retry
any 5xx generic + requestId, retry
No optimistic mutations — issue/return/fine pay/create/update/delete are all
server-confirmed (00-shared/07 §9 ).
Undo: in-form edits only; delete = confirm dialog (soft delete server-side);
no undo for issue/return (record lifecycle instead).
BookIssued / BookReturned events (library.service.ts:141-148,180-191) are on the
domain event bus; notification/email consumers (proposed).