Counting Documents
Part of the Databases with MongoDB section of Coddy's Node.js journey. Lesson 19 of 29.
A dashboard needs numbers, and loading every document only to read the array's length makes the database send them all.
countDocuments(filter) answers how many documents match, without sending any of them. With no filter it counts the whole collection.
const open = await tickets.countDocuments({ status: 'open' });
const all = await tickets.countDocuments();Build the filter from the query the way GET /books does, and pass that same filter to the count. A count that ignores the filter gives the right number for the wrong question: every ticket, when the client asked for the open ones.
Challenge
EasyA dashboard shows how many books and ratings there are. Count them in the database instead of loading every document.
- In
server.js, below the comment// Add GET /stats here., add aGET /statsroute. - It responds with status 200 and the number of documents in each collection. With the starter's books and no ratings, that is:
{ "books": 3, "ratings": 0 } ?genre=scifimakesbookscount only the books of that genre.ratingsalways counts every rating, whatever the genre. Try it: SendGET /stats: you get{ "books": 3, "ratings": 0 }. SendGET /stats?genre=scifi:{ "books": 2, "ratings": 0 }. SendGET /stats?genre=poetry:{ "books": 0, "ratings": 0 }. Copy Dune's_idfromGET /books, then sendPUT /books/<Dune's _id>/ratingswith{ "user": "ada", "stars": 4 }. SendGET /statsagain:{ "books": 3, "ratings": 1 }.
Try it yourself
import express from 'express';
import { MongoClient, ObjectId } from 'mongodb';
const client = new MongoClient('mongodb://localhost:27017');
const db = client.db('shelf');
const books = db.collection('books');
const ratings = db.collection('ratings');
await books.createIndex({ isbn: 1 }, { unique: true });
await books.insertMany([
{ title: 'Dune', author: 'Frank Herbert', year: 1965, isbn: '9780441013593', genre: 'scifi' },
{ title: 'Kindred', author: 'Octavia E. Butler', year: 1979, isbn: '9780807083697', genre: 'scifi' },
{ title: 'Emma', author: 'Jane Austen', year: 1815, isbn: '9780141439587', genre: 'classic' },
]);
const app = express();
app.use(express.json());
// Every error answers { error: message }.
function sendError(res, status, message) {
res.status(status).json({ error: message });
}
// The :id as an ObjectId, or null after answering 400 for a bad id.
function parseId(req, res) {
if (ObjectId.isValid(req.params.id)) {
return new ObjectId(req.params.id);
}
sendError(res, 400, 'Not a valid id');
return null;
}
app.get('/', (req, res) => {
res.send('Bookshelf API');
});
app.get('/books', async (req, res) => {
const filter = {};
if (req.query.author) {
filter.author = req.query.author;
}
if (req.query.from || req.query.to) {
filter.year = {};
if (req.query.from) {
filter.year.$gte = Number(req.query.from);
}
if (req.query.to) {
filter.year.$lte = Number(req.query.to);
}
}
const list = await books.find(filter).toArray();
res.json(list);
});
app.post('/books', async (req, res) => {
const book = req.body;
try {
await books.insertOne(book);
} catch (err) {
if (err.code === 11000) {
return sendError(res, 409, 'That ISBN is already on the shelf');
}
throw err;
}
res.status(201).json(book);
});
app.get('/books/newest', async (req, res) => {
const n = Number(req.query.n) || 3;
const list = await books
.find({}, { projection: { title: 1, year: 1, _id: 0 } })
.sort({ year: -1 })
.limit(n)
.toArray();
res.json(list);
});
app.get('/books/:id', async (req, res) => {
const id = parseId(req, res);
if (!id) return;
const book = await books.findOne({ _id: id });
if (!book) {
return sendError(res, 404, 'No such book');
}
res.json(book);
});
app.patch('/books/:id', async (req, res) => {
const id = parseId(req, res);
if (!id) return;
const result = await books.updateOne({ _id: id }, { $set: req.body });
if (result.matchedCount === 0) {
return sendError(res, 404, 'No such book');
}
res.json(await books.findOne({ _id: id }));
});
app.post('/books/:id/copies', async (req, res) => {
const id = parseId(req, res);
if (!id) return;
const result = await books.updateOne({ _id: id }, { $inc: { copies: 1 } });
if (result.matchedCount === 0) {
return sendError(res, 404, 'No such book');
}
res.json(await books.findOne({ _id: id }));
});
app.post('/books/:id/tags', async (req, res) => {
const id = parseId(req, res);
if (!id) return;
const result = await books.updateOne({ _id: id }, { $push: { tags: req.body.tag } });
if (result.matchedCount === 0) {
return sendError(res, 404, 'No such book');
}
res.json(await books.findOne({ _id: id }));
});
app.delete('/books/:id', async (req, res) => {
const id = parseId(req, res);
if (!id) return;
const result = await books.deleteOne({ _id: id });
if (result.deletedCount === 0) {
return sendError(res, 404, 'No such book');
}
res.status(204).end();
});
app.put('/books/:id/ratings', async (req, res) => {
const id = parseId(req, res);
if (!id) return;
const { user, stars } = req.body;
await ratings.updateOne({ bookId: id, user }, { $set: { stars } }, { upsert: true });
res.json({ user, stars });
});
app.get('/books/:id/ratings', async (req, res) => {
const id = parseId(req, res);
if (!id) return;
res.json(await ratings.find({ bookId: id }).toArray());
});
// Add GET /stats here.
app.listen(3000, () => {
console.log('Bookshelf is running on port 3000');
});
This lesson includes a short quiz. Start the lesson to answer it and track your progress.
All lessons in Databases with MongoDB
Practice on your own: Express playground