MongoDB Best Practices for Modern Web Applications
    August 10, 2026
    19 min read

    MongoDB Best Practices for Modern Web Applications

    Learn practical MongoDB best practices for schema design, indexing, query optimization, data modeling, security, monitoring, and reliable production applications.

    MongoDBDatabaseNoSQLPerformanceData ModelingSecurityScalability

    MongoDB Best Practices for Modern Web Applications

    MongoDB is a flexible document database that works well for many modern web applications. Its document model makes it possible to represent application data naturally, while features such as indexes, aggregation pipelines, transactions, replication, and managed cloud deployments provide the tools needed for production systems.

    However, MongoDB's flexibility does not mean that schema design and query performance can be ignored.

    Poor document modeling, unnecessary indexes, inefficient queries, unbounded arrays, and weak security controls can cause problems as an application grows.

    This guide covers practical MongoDB best practices for designing schemas, creating indexes, optimizing queries, handling relationships, securing databases, monitoring performance, and planning for production reliability.


    MongoDB best practices at a glance

    AreaBest practice
    Schema designDesign around application access patterns
    RelationshipsEmbed when data belongs together; reference when data grows independently
    IndexingCreate indexes around real query patterns
    QueriesReturn only the data you need
    PaginationPrefer cursor-based pagination for large datasets
    AggregationFilter early and avoid unnecessarily large pipelines
    ValidationValidate data at application and database boundaries
    SecurityUse least-privilege access and never expose database credentials
    MonitoringUse explain() and production monitoring
    BackupsMaintain tested backups and recovery procedures
    ScalingPlan for growth before bottlenecks become production incidents

    1. Design schemas around how your application reads data

    One of the most important MongoDB principles is:

    Design your data model around your application's access patterns.

    A relational database often starts with normalization and relationships between tables. MongoDB gives you more flexibility to structure documents around the operations your application performs most frequently.

    Before creating collections, ask:

    • What data is usually read together?
    • Which fields are queried frequently?
    • Which relationships are one-to-one?
    • Which relationships can grow indefinitely?
    • Which data changes independently?
    • How large can a document become?
    • Which queries need to be fast?

    For example, if an application frequently displays a user's profile together with basic account information, embedding those fields may make sense.

    {
      _id: ObjectId("..."),
      name: "John Doe",
      email: "[email protected]",
      profile: {
        bio: "Software developer",
        location: "San Francisco",
        website: "https://johndoe.com"
      }
    }
    

    The important question isn't whether embedding or referencing is universally better.

    The question is:

    How will the application actually use the data?


    2. Embed vs. reference

    MongoDB provides two common approaches for representing relationships.

    Embed related data

    Embedding is useful when related data:

    • is usually accessed together;
    • belongs naturally to the parent document;
    • has a relatively small and bounded size;
    • doesn't need to be independently queried or updated frequently.

    Example:

    {
      _id: ObjectId("..."),
      name: "John Doe",
      email: "[email protected]",
      address: {
        city: "Thiruvananthapuram",
        country: "India"
      }
    }
    

    A single query can retrieve the complete document.


    Reference related data

    References are useful when:

    • related data is large;
    • related data grows independently;
    • the relationship is one-to-many or many-to-many;
    • the related records are frequently accessed separately;
    • embedding would create an unbounded document.

    For example:

    {
      _id: ObjectId("..."),
      title: "My Blog Post",
      author: ObjectId("user_id"),
      tags: [
        ObjectId("tag1"),
        ObjectId("tag2")
      ]
    }
    

    The referenced documents can live in separate collections.


    Embed or reference?

    SituationUsually consider
    Small 1:1 dataEmbed
    Small 1:few relationshipEmbed
    Data always read togetherEmbed
    Large related documentsReference
    Unbounded one-to-many relationshipReference
    Many-to-many relationshipReference
    Related data changes independentlyReference
    Frequently accessed summary dataConsider embedding or denormalization

    There is no rule that says an application must use only one approach.

    A single MongoDB application can use both embedding and references.


    3. Avoid unbounded arrays

    Arrays are useful, but an array that grows forever can become a schema and performance problem.

    For example, this can become problematic:

    {
      _id: ObjectId("..."),
      username: "john",
      notifications: [
        // thousands or potentially millions of items
      ]
    }
    

    Instead, move independently growing data into another collection:

    {
      _id: ObjectId("notification_id"),
      userId: ObjectId("user_id"),
      message: "You received a new application",
      createdAt: ISODate("2026-10-01T10:00:00Z")
    }
    

    Then query notifications separately:

    db.notifications
      .find({ userId: ObjectId("user_id") })
      .sort({ createdAt: -1 })
      .limit(20);
    

    This keeps the main user document bounded.


    4. Use appropriate document sizes

    MongoDB documents have a maximum BSON document size.

    More importantly, even before reaching that limit, excessively large documents can create unnecessary memory usage and transfer overhead.

    Avoid putting every piece of an application's history into one document.

    For example, instead of:

    User
     ├─ profile
     ├─ settings
     ├─ all notifications
     ├─ all login history
     ├─ all transactions
     ├─ all messages
     └─ all activity
    

    consider separate collections for independently growing data:

    users
    notifications
    loginHistory
    transactions
    messages
    activity
    

    Then connect them using identifiers where appropriate.


    5. Design indexes around real queries

    Indexes are one of the most important tools for MongoDB performance.

    Without an appropriate index, MongoDB may need to examine many documents to answer a query.

    For example:

    db.users.find({ email: "[email protected]" });
    

    If email is frequently queried, an index may be appropriate:

    db.users.createIndex({ email: 1 });
    

    Common index types

    Single-field index

    db.users.createIndex({ email: 1 });
    

    Compound index

    db.posts.createIndex({
      authorId: 1,
      publishedAt: -1
    });
    

    This can support queries that filter by authorId and sort or filter using publishedAt.

    Unique index

    db.users.createIndex(
      { email: 1 },
      { unique: true }
    );
    

    This can enforce uniqueness at the database level.

    Partial index

    A partial index only indexes documents that satisfy a condition.

    db.users.createIndex(
      { email: 1 },
      {
        partialFilterExpression: {
          email: { $exists: true }
        }
      }
    );
    

    Partial indexes can be useful when only a subset of documents participates in an important query pattern.


    6. Don't create indexes for everything

    Indexes improve read performance, but they aren't free.

    Every additional index can:

    • consume storage;
    • consume memory;
    • increase write overhead;
    • increase maintenance cost.

    Therefore, don't create an index simply because a field exists.

    Create indexes based on actual query patterns.

    A useful workflow is:

    Identify important query
            ↓
    Measure query performance
            ↓
    Inspect explain()
            ↓
    Create or adjust index
            ↓
    Measure again
    

    7. Understand compound index ordering

    Compound indexes are sensitive to field ordering.

    For example:

    db.orders.createIndex({
      customerId: 1,
      createdAt: -1
    });
    

    This is designed around queries involving customerId and createdAt.

    For example:

    db.orders
      .find({ customerId: ObjectId("...") })
      .sort({ createdAt: -1 })
      .limit(20);
    

    When designing compound indexes, consider:

    • equality filters;
    • sort fields;
    • range conditions;
    • the actual workload;
    • index selectivity;
    • query frequency.

    Don't rely on a generic "always put the most selective field first" rule without testing your actual workload.


    8. Use explain() to understand queries

    Don't guess why a query is slow.

    Use explain().

    For example:

    db.users
      .find({ age: { $gte: 25 } })
      .explain("executionStats");
    

    Look at information such as:

    • execution time;
    • documents examined;
    • keys examined;
    • whether an index was used;
    • how many documents were returned.

    A useful goal is to avoid examining a huge number of documents just to return a small result set.


    9. Return only the fields you need

    If an application only needs a few fields, don't retrieve a large document unnecessarily.

    For example:

    db.users.find(
      { age: { $gte: 18 } },
      {
        name: 1,
        email: 1,
        _id: 0
      }
    );
    

    This projection returns only the requested fields.

    The same principle applies when using MongoDB through an ODM such as Mongoose.

    Reducing unnecessary data can improve:

    • database work;
    • network transfer;
    • application memory usage;
    • serialization/deserialization costs.

    10. Use efficient pagination

    Pagination is common in web applications.

    A simple approach is:

    db.posts
      .find({})
      .skip(10000)
      .limit(20);
    

    This can become inefficient for very large offsets because the database still has to work through skipped records.

    For large or frequently accessed datasets, consider cursor-based pagination.

    For example:

    const lastId = ObjectId("...");
    
    db.posts
      .find({
        _id: { $gt: lastId }
      })
      .sort({
        _id: 1
      })
      .limit(20);
    

    For production systems, design the cursor around the actual sort order and create an index that supports it.

    For example:

    db.posts.createIndex({
      createdAt: -1,
      _id: -1
    });
    

    Then paginate using both fields when ordering by creation time.


    11. Optimize aggregation pipelines

    Aggregation pipelines are powerful for reporting, analytics, filtering, and transformations.

    A common principle is to reduce the amount of data as early as practical.

    For example:

    db.orders.aggregate([
      {
        $match: {
          status: "completed"
        }
      },
      {
        $project: {
          customerId: 1,
          total: 1,
          date: 1
        }
      },
      {
        $group: {
          _id: "$customerId",
          totalSpent: {
            $sum: "$total"
          },
          orderCount: {
            $sum: 1
          }
        }
      },
      {
        $sort: {
          totalSpent: -1
        }
      }
    ]);
    

    The pipeline:

    1. Filters documents.
    2. Keeps only required fields.
    3. Groups the remaining data.
    4. Sorts the result.

    The exact optimization depends on the pipeline and indexes.


    12. Be careful with $lookup

    MongoDB's $lookup can join related collections, but it should not automatically be treated as a replacement for thoughtful data modeling.

    Before using $lookup, consider:

    • how frequently the query runs;
    • how many documents participate;
    • whether the related data could be embedded;
    • whether appropriate indexes exist;
    • whether the result is too large.

    For frequently accessed application paths, schema design can sometimes eliminate the need for an expensive join-like operation.


    13. Use schema validation

    MongoDB is flexible, but flexibility does not mean your data should have no rules.

    At the application layer, Mongoose can define validation:

    const userSchema = new mongoose.Schema({
      email: {
        type: String,
        required: true,
        lowercase: true,
        trim: true
      },
    
      age: {
        type: Number,
        min: 0,
        max: 120
      }
    });
    

    You can also use MongoDB's own schema validation features when database-level validation is appropriate.

    The important principle is:

    Validate data at the boundaries where untrusted or inconsistent data can enter the system.


    14. Don't rely only on frontend validation

    Frontend validation improves user experience.

    It is not a security boundary.

    For example, this frontend validation:

    if (!email) {
      setError("Email is required");
    }
    

    does not mean the backend can safely assume that email is valid.

    The API should validate incoming data independently:

    Browser
       ↓
    Frontend validation
       ↓
    HTTP request
       ↓
    Backend validation
       ↓
    Database validation
    

    The backend should reject malformed or unauthorized data even when the request did not originate from your own frontend.


    15. Prevent injection-style query problems

    Never blindly convert user-controlled objects into MongoDB queries.

    For example, avoid patterns where an entire request body is passed directly into a query:

    // Avoid blindly trusting user input
    User.find(req.body);
    

    Instead, explicitly select and validate the fields your endpoint supports:

    const { email } = req.body;
    
    if (typeof email !== "string") {
      return res.status(400).json({
        error: "Invalid email"
      });
    }
    
    const user = await User.findOne({ email });
    

    Use established validation libraries such as:

    • Zod
    • Joi
    • Yup

    and apply appropriate sanitization and authorization rules.


    16. Use least-privilege database access

    Your application should not connect to MongoDB using an unnecessarily powerful administrative account.

    Create a database user with only the permissions required by the application.

    For example, a deployment may use a dedicated application account with access limited to the application's database.

    Avoid hardcoding credentials:

    // Never do this
    const password = "my-super-secret-password";
    

    Use environment variables or an appropriate secret-management system:

    const password = process.env.MONGODB_PASSWORD;
    

    And never commit secrets to Git.


    17. Secure the MongoDB connection

    A production MongoDB deployment should be protected with appropriate network and authentication controls.

    Important considerations include:

    • authentication enabled;
    • TLS where appropriate;
    • restricted network access;
    • strong credentials;
    • least-privilege database users;
    • secure connection strings;
    • secret management;
    • regular dependency and infrastructure updates.

    For MongoDB Atlas, configure network access and database users according to your application's actual deployment architecture.

    Do not expose a production MongoDB server directly to the public internet without appropriate security controls.


    18. Keep connection strings out of source code

    A MongoDB connection string often contains credentials.

    Avoid:

    mongoose.connect(
      "mongodb+srv://user:[email protected]/myapp"
    );
    

    Prefer:

    mongoose.connect(
      process.env.MONGODB_URI
    );
    

    Then configure:

    MONGODB_URI=...
    

    through the environment or a secure secret-management system.

    Your .env file should normally be excluded from Git:

    .env
    .env.*
    !.env.example
    

    An .env.example can document the required variables without containing real credentials:

    MONGODB_URI=
    JWT_SECRET=
    

    19. Handle connections correctly

    Create a predictable database connection lifecycle.

    For example:

    import mongoose from "mongoose";
    
    async function connectDatabase() {
      try {
        await mongoose.connect(process.env.MONGODB_URI);
        console.log("MongoDB connected");
      } catch (error) {
        console.error("MongoDB connection failed", error);
        process.exit(1);
      }
    }
    
    export default connectDatabase;
    

    In production, also consider:

    • connection pool configuration;
    • startup failure handling;
    • graceful shutdown;
    • health checks;
    • connection monitoring.

    Don't repeatedly create new database connections for every request.


    20. Use transactions when atomic multi-document operations are required

    MongoDB supports transactions for operations that need atomicity across multiple documents or collections.

    For example, if an operation updates several related pieces of data and they must either all succeed or all fail, a transaction may be appropriate.

    Conceptually:

    const session = await mongoose.startSession();
    
    try {
      session.startTransaction();
    
      await Account.updateOne(
        { _id: senderId },
        { $inc: { balance: -100 } },
        { session }
      );
    
      await Account.updateOne(
        { _id: receiverId },
        { $inc: { balance: 100 } },
        { session }
      );
    
      await session.commitTransaction();
    } catch (error) {
      await session.abortTransaction();
      throw error;
    } finally {
      await session.endSession();
    }
    

    Don't use transactions for every operation.

    First design your data model so that common operations can remain simple. Use transactions when the business operation genuinely requires multi-document atomicity.


    21. Use appropriate write and read concerns

    MongoDB provides controls for how reads and writes interact with durability and consistency.

    For critical operations, understand settings such as:

    • writeConcern;
    • readConcern;
    • readPreference.

    For example, financial or transactional workflows may require stronger guarantees than analytics workloads.

    The correct settings depend on the application's consistency, latency, availability, and durability requirements.


    22. Monitor slow operations

    Production monitoring should help you discover inefficient queries before they become major problems.

    MongoDB provides profiling and diagnostic capabilities.

    For example, a development environment can use profiling to investigate slow operations:

    db.setProfilingLevel(1, {
      slowms: 100
    });
    

    Then inspect profile data:

    db.system.profile
      .find()
      .sort({ ts: -1 })
      .limit(10);
    

    Be careful with profiling configuration in production. Choose settings appropriate for your workload and observability requirements.


    23. Monitor queries with explain()

    For a query that seems slow:

    db.orders
      .find({
        status: "completed"
      })
      .sort({
        createdAt: -1
      })
      .explain("executionStats");
    

    Pay attention to:

    executionTimeMillis
    totalDocsExamined
    totalKeysExamined
    nReturned
    

    For example, if:

    nReturned: 20
    totalDocsExamined: 500000
    

    the query may need investigation.

    The exact acceptable ratio depends on the workload, but examining hundreds of thousands of documents to return a handful of records is often a sign that the query or index deserves attention.


    24. Plan backups before production

    A database backup is useful only if you can actually restore it.

    Don't stop at:

    "We have backups."

    Test the recovery process.

    A logical backup can be created with tools such as:

    mongodump \
      --uri="$MONGODB_URI" \
      --out="./backup"
    

    A restore can be performed with:

    mongorestore \
      --uri="$MONGODB_URI" \
      ./backup
    

    The exact backup strategy should depend on:

    • database size;
    • recovery point objectives;
    • recovery time objectives;
    • deployment architecture;
    • managed vs self-hosted MongoDB;
    • compliance requirements.

    25. Test your restore process

    A backup that has never been restored is an assumption, not a verified recovery strategy.

    A practical process is:

    Create backup
         ↓
    Store backup securely
         ↓
    Restore into isolated environment
         ↓
    Verify data
         ↓
    Document recovery procedure
         ↓
    Repeat periodically
    

    Consider keeping backups separate from the primary database infrastructure so a single infrastructure failure does not destroy both the database and its backups.


    26. Understand replica sets and high availability

    MongoDB replica sets provide multiple copies of data and support automatic failover.

    A simplified topology can look like:

                 ┌──────────────┐
                 │   Primary    │
                 └──────┬───────┘
                        │
              ┌─────────┴─────────┐
              ↓                   ↓
       ┌──────────────┐    ┌──────────────┐
       │   Secondary  │    │   Secondary  │
       └──────────────┘    └──────────────┘
    

    Replica sets are important for production deployments that require availability and redundancy.

    If you're using MongoDB Atlas, much of the underlying infrastructure management is handled by the service, but you still need to understand the availability characteristics of the deployment you choose.


    27. Consider sharding only when you need it

    Sharding distributes data across multiple MongoDB servers.

    It can help applications that have workloads beyond what a single deployment can handle.

    But sharding adds architectural complexity.

    Don't introduce sharding simply because an application is growing.

    First investigate:

    • query performance;
    • indexes;
    • schema design;
    • connection pooling;
    • caching;
    • hardware resources;
    • read/write workload;
    • application architecture.

    Use sharding when the workload and scale actually justify it.


    28. Use caching when appropriate

    MongoDB should not necessarily handle every repeated read directly.

    For frequently requested data, an application-level cache can reduce database load.

    For example:

    Client
      ↓
    Express API
      ↓
    Cache
      ├─ hit → return data
      │
      └─ miss
           ↓
        MongoDB
    

    Common caching technologies include Redis-compatible systems and application-level caching.

    Caching introduces its own challenges, especially cache invalidation and stale data.

    Use it where measurement shows that caching provides meaningful benefits.


    29. Avoid premature optimization

    MongoDB performance tuning should be based on actual workloads.

    Don't automatically:

    • create dozens of indexes;
    • add Redis;
    • introduce sharding;
    • denormalize everything;
    • use transactions everywhere;
    • add complicated aggregation pipelines.

    Instead:

    Build
      ↓
    Measure
      ↓
    Identify bottleneck
      ↓
    Optimize
      ↓
    Measure again
    

    This keeps the architecture understandable while still allowing it to evolve as the application grows.


    30. Common MongoDB mistakes

    Here are some problems worth watching for.

    Mistake 1: No indexes on important queries

    db.users.find({ email: "[email protected]" });
    

    If this query runs frequently, evaluate whether an index is needed.

    Mistake 2: Indexing every field

    Too many indexes increase storage and write overhead.

    Mistake 3: Unbounded arrays

    Avoid documents that continuously grow with user activity.

    Mistake 4: Returning entire documents

    Use projection when only a subset of fields is needed.

    Mistake 5: Deep pagination with large skip() values

    Consider cursor-based pagination for large datasets.

    Mistake 6: Trusting frontend validation

    Validate and authorize requests on the server.

    Mistake 7: Hardcoding credentials

    Use environment variables or a secret manager.

    Mistake 8: Never testing backups

    Regularly test restoration.

    Mistake 9: Optimizing without measurements

    Use explain(), monitoring, and real workload measurements before changing the schema or indexes.


    31. A practical MongoDB production checklist

    Before deploying a MongoDB-backed application, review the following:

    • Schema designed around real access patterns.
    • Relationships modeled intentionally.
    • Unbounded arrays avoided.
    • Important queries identified.
    • Appropriate indexes created.
    • Unused indexes reviewed.
    • Slow queries analyzed with explain().
    • Large queries use projection where appropriate.
    • Pagination strategy chosen intentionally.
    • Aggregation pipelines reviewed.
    • Input validation implemented.
    • Authorization enforced on the backend.
    • Database users follow least privilege.
    • Credentials stored securely.
    • Network access restricted appropriately.
    • TLS configured where required.
    • Connection handling implemented correctly.
    • Transactions used where business operations require them.
    • Monitoring configured.
    • Backups configured.
    • Restore procedure tested.
    • High-availability requirements reviewed.
    • Scaling requirements understood.

    32. A simple MongoDB optimization workflow

    When a MongoDB query becomes slow, follow a structured process.

    Identify slow query
           ↓
    Measure current performance
           ↓
    Run explain("executionStats")
           ↓
    Check indexes
           ↓
    Review query shape
           ↓
    Review returned fields
           ↓
    Review schema design
           ↓
    Optimize
           ↓
    Measure again
    

    This is usually more effective than adding indexes randomly.


    Final thoughts

    MongoDB's flexibility is one of its biggest strengths, but good MongoDB applications still require deliberate data modeling and performance engineering.

    The most important principles are:

    1. Design schemas around real application access patterns.
    2. Embed related data when it belongs together and stays bounded.
    3. Reference data that grows independently or becomes too large.
    4. Create indexes based on real query patterns.
    5. Use explain() to investigate query performance.
    6. Return only the data your application needs.
    7. Use cursor-based pagination for large datasets where appropriate.
    8. Validate and authorize data on the server.
    9. Use least-privilege database accounts.
    10. Protect database credentials and network access.
    11. Use transactions when atomic multi-document operations actually require them.
    12. Monitor production workloads instead of guessing.
    13. Maintain backups and regularly test restoration.
    14. Scale only when measurements show that additional infrastructure is necessary.

    MongoDB gives you a flexible document model, but flexibility should be paired with discipline.

    The best MongoDB schema is not the one with the most normalization or the most denormalization. It is the one that matches the application's real access patterns while remaining secure, maintainable, and efficient as the workload grows.

    Found this helpful?

    Follow me for more development insights and tutorials