@mongalayer/client

Batch operations

Run multiple read operations in a single request with the client SDK.

Batch operations let you bundle several read operations into a single HTTP request. Instead of issuing one request per operation, you send them all at once and receive a tuple of results back, fully typed and in the same order as the operations you provided.

This is useful for reducing round-trips when a view or screen needs data from multiple collections or multiple queries at the same time.

Batch operations are only available when the client uses the "json"format. Calling batch() while the client is in "routed" format throws an error, since routed requests rely on URL path segments that cannot describe multiple operations.

Supported operations

Only read operations can be batched:

OperationDescription
findOneFind a single document matching a filter
findFind multiple documents matching a filter
aggregateRun an aggregation pipeline

Usage

Call batch() on a Db instance with an array of operations. Each operation is created with the BatchOperation helper, which provides typed factory methods for every supported operation.

import { MongalayerClient, BatchOperation } from "@mongalayer/client";

const client = new MongalayerClient("http://localhost");
const db = client.db("app");

const [project, projectAssets, assetStats] = await db.batch([
    BatchOperation.findOne<Project>("projects", {
        filter: { _id: "project-123" },
    }),
    BatchOperation.find<Asset>("assets", {
        filter: { projectID: "project-123" },
        options: { sort: { supply: -1 } }
    }),
    BatchOperation.aggregate<Asset>("assets", {
        pipeline: [
            { $match: { projectID: "project-123" } },
            { $group: { _id: "$status", count: { $sum: 1 } } }
        ]
    })
]);
// project: Project | null
// projectAssets: Asset[]
// assetStats: { _id: string; count: number }[]

The result is a tuple whose types match each operation in order:

  • findOne returns TSchema | null
  • find returns TSchema[]
  • aggregate returns the projected result type []

Defining operations

There are three ways to define an operation in a batch. They are interchangeable and can be mixed within the same batch — pick whichever reads best for your code.

To get the most accurate result types, the collection's schema type must reach BatchOperation. The recommended way to do this is to type the collection name as a CollectionName<TSchema>, either through a variable, a helper function, or an inline assertion.

import { MongalayerClient, BatchOperation } from "@mongalayer/client";
import type { CollectionName } from "@mongalayer/client";

const db = new MongalayerClient("http://localhost").db("app");

// A typed collection name can be reused across operations
const users: CollectionName<User> = "users";

Static methods

The factory methods (findOne, find, aggregate) are the most convenient option. When you cannot supply a typed CollectionName, pass the schema as a generic to keep result inference intact.

await db.batch([
    // Typed collection name — operation and schema are inferred
    BatchOperation.find(users, {
        filter: { roles: { $in: ["admin"] } }
    }),
    // Schema passed as a generic — preferred when using a plain string name
    BatchOperation.findOne<Project>("projects", {
        filter: { _id: "project-123" }
    })
]);

Constructor

Use new BatchOperation(collection, operation, payload) when you prefer constructing instances directly. Prefer passing a typed CollectionName so the operation argument narrows the payload and result types.

await db.batch([
    new BatchOperation(users, "find", {
        filter: { roles: { $in: ["admin"] } }
    })
]);
Avoid supplying the schema as the constructor's class generic (new BatchOperation<User>(...)). Doing so breaks inference for the operation argument, so the result widens to a union of all supported operation return types. Use a typed CollectionName or the static methods instead.

Object literals

You can also pass plain objects matching the BatchOperation shape, which is handy when operations are built dynamically. Type the collection field as a CollectionName<TSchema> to retain result types.

await db.batch([
    {
        collection: "users" as CollectionName<User>,
        operation: "find",
        payload: {
            filter: { roles: { $in: ["admin"] } }
        }
    }
]);

Mixing across collections

A single batch can combine all three styles over different collections. The result tuple stays fully typed in order:

const [activeUsers, projectA, nearbyStats] = await db.batch([
    new BatchOperation(users, "find", {
        filter: { roles: { $in: ["admin"] } }
    }),
    BatchOperation.findOne("projects" as CollectionName<Project>, {
        filter: { _id: "project-123" }
    }),
    {
        collection: "assets" as CollectionName<Asset>,
        operation: "aggregate",
        payload: {
            pipeline: [
                { $match: { projectID: "project-123" } },
                { $group: { _id: "$status", count: { $sum: 1 } } }
            ]
        }
    }
]);
// admins:      User[]
// projectA:    Project | null
// nearbyStats: { _id: string, count: number }[]
If the collection name is an untyped string, operations fall back to the generic Document schema, so filters and results are not type-checked against a specific shape.

BatchOperation helpers

Each factory method takes the target collection name and the operation payload.

BatchOperation.findOne(collection, payload)

BatchOperation.findOne<Project>("projects", {
  filter: { _id: "project-123" },
  options: { projection: { name: 1 } }
});
Payload fieldTypeDescription
filterFilter<TSchema>The filter to match a document
optionsFindOneOptionsOptional projection and sort

BatchOperation.find(collection, payload)

BatchOperation.find<Asset>("assets", {
  filter: { projectID: "project-123" },
  options: { limit: 10, sort: { supply: -1 } }
});
Payload fieldTypeDescription
filterFilter<TSchema>The filter to match documents
optionsFindOptionsOptional projection, limit, skip and sort

BatchOperation.aggregate(collection, payload)

BatchOperation.aggregate<Asset>("assets", {
  pipeline: [
    { $match: { projectID: "project-123" } },
    { $group: { _id: "$status", count: { $sum: 1 } } }
  ]
});
Payload fieldTypeDescription
pipelineDocument[]The aggregation pipeline stages
optionsAggregateOptionsOptional aggregation options

Automatic batching

Instead of constructing batches by hand, you can let the client batch operations for you. When autoBatch is enabled, supported read operations (find, findOne, aggregate) called through the normal collection API are queued and combined into a single batch request automatically.

const client = new MongalayerClient("http://localhost", { autoBatch: true });
const db = client.db("app");

// These three calls are automatically batched together in one request
const user = await db.collection<User>("users").findOne({ _id: "user-1" });
const projects = await db.collection<Project>("projects").find({ owner: "user-1" });
const stats = await db.collection<Asset>("assets").aggregate([{ $group: { _id: "$status", count: { $sum: 1 } } }]);

Operations are collected for a short window — controlled by autoBatchDelay (default 10 ms) — and then flushed together. Only the batchable read operations participate; all other operations are sent as individual requests.

Per-collection control

You can enable or disable auto-batching for a specific collection, regardless of the client-wide setting, by passing the autoBatch option when creating the collection:

// Force batching on for this collection even if the client default is off
const users = db.collection<User>("users", { autoBatch: true });

// Opt this collection out even if the client default is on
const audit = db.collection<Log>("audit", { autoBatch: false });

Opting a single call out

Use the withoutBatch getter to run one call outside of auto-batching while keeping the collection's batching behavior for everything else:

const users = db.collection<User>("users");

// Sent as its own request, bypassing the auto-batch queue
const user = await users.withoutBatch.findOne({ _id: "user-1" });

Context

Like collection methods, batch() accepts an optional context parameter as its last argument. The context applies to the whole batch request:

await db.batch(
  [
    BatchOperation.find<Project>("projects", { filter: { owner: "user-1" } })
  ],
  { requestId: "abc-123" }
);

See context for details on how the value is encoded and used.

Each operation in a batch is still subject to the same access control rules as its standalone equivalent. Permissions are evaluated per operation.
Copyright © 2026