iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Schema Design

MongoDB is schemaless on the wire but every real application has a schema — its just enforced in your code or at the DB. Modern Mongo gives you both: JSON-Schema validators at the collection level, and JSON-Schema-aware libraries (Mongoose, Zod adapters) at the app level. Decide once and write it down.

Collection validators + Mongoose schema side by side

EXAMPLE
// 1) MongoDB-level JSON Schema validator — checked on every insert/update
db.createCollection('orders', {
  validator: {
    $jsonSchema: {
      bsonType: 'object',
      required: ['_id', 'customer', 'total_cents', 'status', 'created_at'],
      additionalProperties: false,
      properties: {
        _id:          { bsonType: 'string', maxLength: 24 },
        customer:     { bsonType: 'string', minLength: 1, maxLength: 120 },
        total_cents:  { bsonType: 'long',   minimum: 0 },
        currency:     { bsonType: 'string', enum: ['AUD', 'USD', 'NZD'] },
        status:       { bsonType: 'string',
                         enum: ['new', 'paid', 'shipped', 'cancelled', 'refunded'] },
        items: {
          bsonType: 'array',
          minItems: 1,
          items: {
            bsonType: 'object',
            required: ['sku', 'qty', 'price_cents'],
            properties: {
              sku:         { bsonType: 'string' },
              qty:         { bsonType: 'int', minimum: 1 },
              price_cents: { bsonType: 'long', minimum: 0 },
            },
          },
        },
        created_at:   { bsonType: 'date' },
        paid_at:      { bsonType: ['date', 'null'] },
      },
    },
  },
  validationLevel:  'moderate',   // existing docs that fail validator can still be updated
  validationAction: 'error',      // reject (vs 'warn' for soft launch)
});

// 2) Add or change validator on an EXISTING collection
db.runCommand({
  collMod: 'orders',
  validator: { /* updated schema here */ },
  validationLevel: 'strict',
});

// 3) Indexes that go alongside the schema (defined in the same migration)
db.orders.createIndex({ customer: 1, created_at: -1 });
db.orders.createIndex({ status: 1, created_at: -1 });
db.orders.createIndex({ 'items.sku': 1 });

// 4) App-side schema with Mongoose — matched 1:1 with the DB validator
import { Schema, model } from 'mongoose';

const ItemSchema = new Schema({
  sku:         { type: String, required: true },
  qty:         { type: Number, min: 1, required: true },
  price_cents: { type: Number, min: 0, required: true },
}, { _id: false });

const OrderSchema = new Schema({
  _id:         { type: String, required: true },
  customer:    { type: String, required: true, trim: true, maxlength: 120 },
  total_cents: { type: Number, min: 0, required: true },
  currency:    { type: String, enum: ['AUD','USD','NZD'], default: 'AUD' },
  status:      { type: String, enum: ['new','paid','shipped','cancelled','refunded'], default: 'new' },
  items:       { type: [ItemSchema], validate: (v: any[]) => v.length > 0 },
  paid_at:     Date,
}, { timestamps: { createdAt: 'created_at', updatedAt: 'updated_at' } });

OrderSchema.index({ customer: 1, created_at: -1 });
export const Order = model('Order', OrderSchema);

// 5) The two layers complement each other:
//    - App-side schema: rich types, hooks, virtuals, fast feedback in tests
//    - DB-side validator: catches drift from any other writer (mongo shell, scripts, other services)
// Run both; ship one without the other and a bad write sneaks in.

// 6) Versioning the schema — keep a 'schema_version' field on every doc and a migration plan
db.orders.updateMany({ schema_version: { $lt: 2 } }, [
  { $set: { tier: { $cond: [{ $gte: ['$total_cents', 50000] }, 'gold', 'silver'] },
            schema_version: 2 } },
]);

Why it matters

Layered schema is the right answer: app-side for ergonomics, DB-side for safety. The app schema catches typos and types in tests; the DB validator stops a misbehaving migration script or admin console from writing garbage on a Friday afternoon. Run the same JSON Schema in both places where you can.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
// Schema is flexible but design matters.
// One-to-few: embed. One-to-many: usually reference.
Try it Yourself »

Discussion

Loading…