S
Crud

Bulk Create

Insert many documents in one request with the createBulk handler, with per-item validation and a size limit

The createBulk handler inserts many documents in one request. The body is a JSON array, every item is checked with the validation group of the route, and the response lists the created documents. This page covers the route, the size limits, what happens when an item is invalid or the database refuses one, and how the owner is stamped on each item.

Declare a Bulk Create Route

A bulk create route is a route whose handler is "createBulk". It needs the same two things as a create route, a validation group and an access level. Naming the create group on both routes means that one set of rules guards both ways in:

TypeScript
// src/collections/products.ts
import { collection, defineRoutes, fields } from "@starklabs/forge";
import { z } from "zod";

export const products = collection({
  type: "crud",
  route: "products",
  model: "Product",
  schema: {
    name: fields.requiredString,
    price: fields.requiredNumber,
    sku: { mongoose: { type: String, unique: true, sparse: true }, zod: z.string().optional() },
  },
  validations: {
    create: {
      name: z.string().trim().min(1).max(120),
      price: z.number().min(0),
      sku: z.string().optional(),
    },
  },
  routes: defineRoutes([
    { method: "post", path: "/", handler: "create", auth: "authenticated", roles: ["editor", "admin"], validation: "create" },
    {
      method: "post",
      path: "/bulk",
      handler: "createBulk",
      auth: "authenticated",
      roles: ["editor", "admin"],
      validation: "create",
      config: { maxBulkSize: 50 },
    },
  ]),
});

With the collection route products, the second route makes POST /api/v1/products/bulk. The path /bulk is a convention, and you can choose another one. config.maxBulkSize limits the number of items per request. It is 100 when you leave it out.

Send a Request

The body is a JSON array. Each item has the shape that a single create request would have:

Text
POST /api/v1/products/bulk
Content-Type: application/json

[
  {
    "name": "Monitor arm",
    "price": 59,
    "sku": "MA-004"
  },
  {
    "name": "Webcam",
    "price": 49,
    "sku": "WC-005"
  }
]
Text
HTTP/1.1 201 Created
Content-Type: application/json

{
  "success": true,
  "message": "2 Product items created successfully.",
  "data": [
    {
      "name": "Monitor arm",
      "price": 59,
      "sku": "MA-004",
      "_id": "665f1c2e8b3a4d0012a10004",
      "createdAt": "2026-01-15T10:30:00.000Z",
      "updatedAt": "2026-01-15T10:30:00.000Z"
    },
    {
      "name": "Webcam",
      "price": 49,
      "sku": "WC-005",
      "_id": "665f1c2e8b3a4d0012a10005",
      "createdAt": "2026-01-15T10:30:00.000Z",
      "updatedAt": "2026-01-15T10:30:00.000Z"
    }
  ]
}

The message counts the items, and data lists the created documents in the order of the request.

Size Limits

Three limits apply to a bulk request:

RequestStatusMessage
A body that is not an array, or an empty array400Send a non-empty JSON array.
More items than maxBulkSize400You can create at most 50 items at once., with the limit of the route in place of 50
A body larger than the bodyLimit option413Request body is too large.

A request with 51 items against a limit of 50 answers like this:

Text
HTTP/1.1 400 Bad Request
Content-Type: application/json

{
  "success": false,
  "message": "You can create at most 50 items at once.",
  "code": "BAD_REQUEST",
  "requestId": "45f262a6-e79b-4796-bd0e-c20305df0222"
}
Size the Batch

The whole array counts toward bodyLimit, which is "100kb" by default. A batch of large documents can reach it before it reaches maxBulkSize. Raise bodyLimit in Configuration, or send smaller batches.

Validation Is All or Nothing

StarkForge checks every item before it saves any of them. When one or more items break a rule, the response is 422 and nothing is saved. Every problem is listed, and the position of its item, counted from 0, comes first in path. This request has a bad second item:

Text
POST /api/v1/products/bulk
Content-Type: application/json

[
  {
    "name": "Monitor arm",
    "price": 59,
    "sku": "MA-004"
  },
  {
    "name": "",
    "price": -1
  }
]
Text
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json

{
  "success": false,
  "message": "1.name: Too small: expected string to have >=1 characters",
  "code": "VALIDATION_FAILED",
  "issues": [
    {
      "path": "1.name",
      "message": "Too small: expected string to have >=1 characters",
      "code": "too_small"
    },
    {
      "path": "1.price",
      "message": "Too small: expected number to be >=0",
      "code": "too_small"
    }
  ],
  "requestId": "0982a467-942a-4faf-b599-32eed7437188"
}

The first item is valid, and it was not saved either. Fix the items that issues names and send the whole array again.

Saving Is Not Atomic

Once every item has passed validation, the items are inserted in order with one database command. MongoDB stops that command at the first error and keeps the items it has already inserted, and StarkForge does not wrap the command in a transaction. The usual cause is a duplicate value in a field that the schema declares unique. In this request, the third item repeats the sku of the first one:

Text
POST /api/v1/products/bulk
Content-Type: application/json

[
  {
    "name": "Webcam",
    "price": 49,
    "sku": "WC-005"
  },
  {
    "name": "Headset",
    "price": 79,
    "sku": "HS-006"
  },
  {
    "name": "Webcam again",
    "price": 49,
    "sku": "WC-005"
  },
  {
    "name": "Microphone",
    "price": 99,
    "sku": "MC-007"
  }
]
Text
HTTP/1.1 409 Conflict
Content-Type: application/json

{
  "success": false,
  "message": "An item with this sku already exists.",
  "code": "DUPLICATE_KEY",
  "hint": "'sku' is declared unique in the schema.",
  "details": {
    "fields": [
      "sku"
    ]
  },
  "requestId": "296e60af-dc68-465b-bd49-a635e034aa86"
}

The first two items were saved before the failure, and the fourth was never tried. A list of the collection shows it:

Text
GET /api/v1/products
Text
HTTP/1.1 200 OK
Content-Type: application/json

{
  "success": true,
  "message": "Product list fetched successfully.",
  "data": [
    {
      "name": "Headset",
      "price": 79,
      "sku": "HS-006",
      "_id": "665f1c2e8b3a4d0012a10001",
      "createdAt": "2026-01-15T10:30:00.000Z",
      "updatedAt": "2026-01-15T10:30:00.000Z"
    },
    {
      "name": "Webcam",
      "price": 49,
      "sku": "WC-005",
      "_id": "665f1c2e8b3a4d0012a10002",
      "createdAt": "2026-01-15T10:30:00.000Z",
      "updatedAt": "2026-01-15T10:30:00.000Z"
    }
  ],
  "meta": {
    "page": 1,
    "limit": 50,
    "total": 2,
    "pages": 1
  }
}
Partial Saves

When the database refuses an item, the items before it stay saved, and the response is still an error. Read the collection before you send the array again, or leave out the items that are already there.

Owner and Protected Fields

Each item is treated the way a single create request is treated. On a collection with an owner field, StarkForge stamps the user of the session on every item and ignores an owner in the body. A caller who is not an administrator also loses the fields of the protectedFields option, which is role by default. This request creates two notes for the user who is signed in:

Text
POST /api/v1/notes/bulk
Content-Type: application/json

[
  {
    "title": "Shopping list"
  },
  {
    "title": "Packing list"
  }
]
Text
HTTP/1.1 201 Created
Content-Type: application/json

{
  "success": true,
  "message": "2 Note items created successfully.",
  "data": [
    {
      "title": "Shopping list",
      "owner": "665f1c2e8b3a4d0012a10003",
      "_id": "665f1c2e8b3a4d0012a10004",
      "createdAt": "2026-01-15T10:30:00.000Z",
      "updatedAt": "2026-01-15T10:30:00.000Z"
    },
    {
      "title": "Packing list",
      "owner": "665f1c2e8b3a4d0012a10003",
      "_id": "665f1c2e8b3a4d0012a10005",
      "createdAt": "2026-01-15T10:30:00.000Z",
      "updatedAt": "2026-01-15T10:30:00.000Z"
    }
  ]
}

The route settings encryptedFields and hashedFields apply to every item, with the same rules as Create. Each hash is slow on purpose, and StarkForge hashes the items one after another, so a bulk request with a hashed field takes longer the more items it holds.

Actions and Files

A before action runs once, before the body is checked. An after action runs once, with ctx.item set to the array of created documents, and a value that it returns replaces data in the response. Actions describes the full context.

The createBulk handler reads JSON only. A files setting on a bulk route stops the app when it starts, so use create when documents need uploaded files.

Errors

StatuscodeWhen it happensWhat to change
400BAD_REQUESTThe body is not an array, the array is empty, or it holds more than maxBulkSize itemsSend between 1 and maxBulkSize items
401ACCESS_TOKEN_MISSINGThe route needs sign-in and no token or cookie was sentSign in first
403ROLE_NOT_ALLOWEDThe route lists roles and the caller's role is not among themSign in with an allowed role
409DUPLICATE_KEYAn item repeats a value of a unique field, in the request or in the collectionSend unique values, and check what was saved
413PAYLOAD_TOO_LARGEThe body is larger than bodyLimitSend fewer items, or raise bodyLimit
422VALIDATION_FAILEDOne or more items break the validation groupFix the items that issues names

Where to Go Next

Validation

Write the rules that every item of a bulk request must pass.

Relations

Reference other documents and choose which of their fields come back.

:::