Skip to main content

Reading Queue API

This page documents the ReadingQueueModule endpoints added under /api/v1/reading-queue.

Data model​

The ReadingQueueItem model in prisma/schema.prisma:

ColumnTypeNotes
idStringUUID primary key
userIdStringFK → User, cascade delete
bookIdStringFK → Book, cascade delete
positionInt0-based ordering index
addedAtDateTimeTimestamp of when the book was added

Constraints:

  • @@unique([userId, bookId]) — a book may only appear once per user's queue
  • @@index([userId, position]) — efficient ordered reads

Base path​

/api/v1/reading-queue

All endpoints require a JWT (Authorization: Bearer <token>).

Endpoints​

GET /​

Returns the authenticated user's queue ordered by position ascending.

Auth: JWT required.

Response — array of ReadingQueueItemDto:

[
{
"id": "uuid",
"bookId": "uuid",
"position": 0,
"addedAt": "2026-04-12T02:39:08.000Z",
"title": "Dune",
"authors": ["Frank Herbert"],
"hasCover": true,
"coverUpdatedAt": "2026-04-12T00:00:00.000Z",
"formats": ["EPUB"],
"hasFileMissing": false
}
]

POST /​

Adds a book to the end of the queue (idempotent — if the book is already present, the request succeeds without creating a duplicate).

Auth: JWT required.

Request body:

{ "bookId": "uuid" }

Response — the created (or existing) ReadingQueueItemDto.

Returns 404 if bookId does not exist in the library.


DELETE /:bookId​

Removes a book from the queue by bookId (not queue item id). Silently succeeds if the book is not in the queue.

Auth: JWT required.

Response: 204 No Content


PUT /order​

Replaces the full queue order. Accepts the desired bookId sequence and assigns positions 0, 1, 2, … in a single database transaction.

Auth: JWT required.

Request body:

{ "bookIds": ["uuid-a", "uuid-b", "uuid-c"] }

Response: 200 OK (empty body)

The full-array approach is used rather than per-item swaps because @dnd-kit produces a new full sorted array on drag end. This keeps the server simple and eliminates partial-update race conditions.


inReadingQueue on book detail​

BooksService.findOne includes a readingQueue relation filtered to the requesting user:

readingQueue: { where: { userId }, select: { id: true } }

The BookDetailDto exposes this as inReadingQueue: boolean (readingQueue.length > 0). The web frontend uses this to initialise the "In Reading Queue" button state when opening the book detail panel, avoiding a separate queue fetch.


Service implementation notes​

Idempotent addToQueue​

ReadingQueueService.addToQueue uses upsert with a no-op update: {} to handle the duplicate case safely under concurrent requests:

await this.prisma.readingQueueItem.upsert({
where: { userId_bookId: { userId, bookId } },
update: {},
create: { userId, bookId, position: nextPosition },
});

nextPosition is derived from MAX(position) + 1 within the user's queue, defaulting to 0 for an empty queue.

Full-array reorder transaction​

reorderQueue issues one updateMany per item inside a $transaction:

await this.prisma.$transaction(
bookIds.map((bookId, position) =>
this.prisma.readingQueueItem.updateMany({
where: { userId, bookId },
data: { position },
}),
),
);

This keeps positions consistent even if the client sends a subset of book IDs (items not mentioned retain their old positions, which is a valid edge-case behaviour).