This chapter follows Authentication in MongoDB. It uses the official mongodb driver and assumes you already have a connected Db from Installing and connecting to MongoDB.
Documents, BSON, and ObjectId
A document is a JSON-like object stored as BSON (Binary JSON). BSON adds types JSON lacks: ObjectId, Date, Decimal128, Binary, and more. Every inserted document gets an _id field; if you omit it, the server generates an ObjectId.
Collections and databases are created implicitly on first write—there is no separate CREATE DATABASE step.
import { ObjectId } from 'mongodb';
export interface Product { name: string; price: number; tags: string[]; createdAt: Date;}
export type ProductDoc = Product & { _id: ObjectId };Use Collection<Product> for inserts and WithId<Product> when reading:
import { Collection, Db, ObjectId, OptionalId, WithId } from 'mongodb';import type { Product } from '../types/product.js';
export function products(db: Db): Collection<Product> { return db.collection<Product>('products');}Create: insertOne and insertMany
export async function createProduct( col: Collection<Product>, data: OptionalId<Product>,) { const result = await col.insertOne(data); return result.insertedId;}
export async function createMany( col: Collection<Product>, items: OptionalId<Product>[],) { const result = await col.insertMany(items); return result.insertedIds;}insertedId and insertedIds tell you what the server assigned.
Read: find, findOne, projection, sort
find() returns a cursor; materialize with toArray(), iteration, or for await.
const cursor = col.find({ price: { $gte: 10 } }) .project({ name: 1, price: 1 }) .sort({ price: -1 }) .limit(20);
const page: WithId<Product>[] = await cursor.toArray();const one = await col.findOne({ name: 'Apple' });Common query operators:
| Operator | Meaning |
|---|---|
$eq, $ne |
Equal / not equal |
$gt, $gte, $lt, $lte |
Comparisons |
$in, $nin |
Membership |
$regex |
Pattern match (use index carefully) |
$and, $or |
Boolean composition |
Count with countDocuments(filter)—avoid deprecated cursor .count().
Update: operators and upsert
Updates use update operators; passing a plain document without operators replaces fields incorrectly in some APIs.
await col.updateOne( { name: 'Apple' }, { $set: { price: 1.2 }, $inc: { version: 1 } },);
await col.updateOne( { sku: 'NEW-001' }, { $setOnInsert: { name: 'New item', createdAt: new Date() } }, { upsert: true },);Array updates:
await col.updateOne( { _id: id }, { $push: { tags: 'sale' }, $addToSet: { tags: 'organic' } },);
await col.updateOne( { _id: id, 'items.sku': 'A1' }, { $set: { 'items.$.qty': 3 } },);
await col.updateOne( { _id: id }, { $pull: { tags: 'sale' } },);findOneAndUpdate returns the document before or after the change—useful for atomic counters:
const updated = await col.findOneAndUpdate( { _id: id }, { $inc: { views: 1 } }, { returnDocument: 'after' },);Delete and replace
await col.deleteOne({ _id: id });await col.deleteMany({ archived: true });await col.replaceOne({ _id: id }, { name: 'Only these fields remain', price: 0 });replaceOne replaces the entire document except _id; prefer $set for partial updates.
bulkWrite
Batch mixed operations in one round trip:
await col.bulkWrite([ { insertOne: { document: { name: 'Bulk', price: 1, tags: [], createdAt: new Date() } } }, { updateOne: { filter: { name: 'Bulk' }, update: { $set: { price: 2 } } } }, { deleteOne: { filter: { name: 'Bulk' } } },]);ObjectId versus string at API boundaries
JSON has no ObjectId type. HTTP clients send _id as a 24-character hex string. Validate before querying:
import { ObjectId } from 'mongodb';
export function parseObjectId(value: string): ObjectId { if (!ObjectId.isValid(value)) { throw new Error('Invalid id'); } return new ObjectId(value);}Expose string ids in REST responses (id.toHexString()) and parse on input. Using strings everywhere in your domain layer is fine if you convert at the repository boundary.
Next: Schema design in MongoDB.
Check your understanding
Question 1 of 7
0 of 7 checked