without populate

export async function getBooks(req, res) {
  const books = await Book.find().populate("authors");
  res.json(books);
}

What happens without populate: book.authors is an array of ObjectId strings. The UI must resolve those ids to author details itself (or ask the server to).

Naive client approach: map ids -> N requests (one per author). Works but is slow and causes N+1 requests.

Example (works, but not optimal):

// book.authors = ['691c6...', '691d...']
const names = await Promise.all(
  book.authors.map(id =>
    fetch(`/authors/${id}`).then(r => {
      if (!r.ok) throw new Error('author fetch failed');
      return r.json();
    }).then(a => a.name)
  )
);
book.authors = names; // replace ids with names

Better: batch on the server — add an endpoint that accepts multiple ids and returns the authors in one query:

// GET /authors?ids=691c6...,691d...
// Server (Express + Mongoose)
router.get('/authors', async (req, res) => {
  const ids = (req.query.ids || '').split(',').filter(Boolean);
  const authors = await Author.find({ _id: { $in: ids } }, 'name'); // only name field
  res.json(authors);
});

Client side:

const ids = book.authors.join(",");
const resp = await fetch(`/authors?ids=${encodeURIComponent(ids)}`);
const authors = await resp.json();
const map = new Map(authors.map((a) => [a._id, a.name]));
book.authors = book.authors.map((id) => map.get(id) || null);


Bilan : Keep using populate server-side so the UI gets names in one response.

  • Server-side aggregation to return books with only needed author fields.
  • populate('authors', 'name') server-side so the UI gets names in one response.