Data Layer

A Data Layer Behind an Interface

data/index.js holds no logic. It is the contract, written as a comment and one export, and the only file MongoDB edits when the records move into MongoDB 1,815 .

src/data/index.js — the whole data layer contractJavaScript
// The store contract. Every method is async and returns plain objects.
// books.list({ limit, offset, q, genre, authorId, sort }) -> { total, items }
// books.get(id) / create(input) / update(id, patch) -> book|null   remove(id) -> boolean
// authors.list({ limit, offset })   authors.get(id)   users.findByEmail(email)
// reviews.listForBook(bookId, { limit, offset })   get(id) / create(input) / remove(id)
// reviews.findByUserAndBook(userId, bookId) -> review|null
// Chapter 4 adds createMongoStore() with these methods and changes only the line below.
import { createMemoryStore } from './memory.js';
export const createStore = createMemoryStore;

Four properties make the swap possible. It returns plain objects: a service calling book.save() is welded to Mongoose 243,355 , which is why MongoDB ends its reads with .lean(). It is async throughout, even in memory where nothing awaits, so a network round trip changes no caller. Collection reads return { total, items }, because offset pagination needs a count. And createStore() is a factory, so each test gets a clean catalog with no teardown.

src/data/memory.js — one method per contract entry (excerpt)JavaScript
export function createMemoryStore() {
  const books = structuredClone(seedBooks);     // per-instance copies, not the seeds
  let nextBook = books.length + 1;
  return { kind: 'memory', books: {
    async list({ limit, offset, q, genre, authorId, sort }) {
      let rows = books;
      if (q) rows = rows.filter((b) => b.title.toLowerCase().includes(q.toLowerCase()));
      if (genre) rows = rows.filter((b) => b.genre === genre);
      if (authorId) rows = rows.filter((b) => b.authorId === authorId);
      const dir = sort?.startsWith('-') ? -1 : 1, key = sort?.replace(/^-/, '') ?? 'title';
      rows = [...rows].sort((a, b) => (a[key] > b[key] ? dir : a[key] < b[key] ? -dir : 0));
      return { total: rows.length, items: clone(rows.slice(offset, offset + limit)) };
    },
    async get(id) { return clone(books.find((b) => b.id === id) ?? null); }
  } };   // then create, update, remove, and the authors, reviews and users groups
}

The clone helper — structuredClone guarded for null — stops a class of phantom bug. Without it books.get('bk_1') hands out a live reference into the array, a service that mutates what it received writes through to the store, and tests start depending on each other.