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:
// 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:
POST /api/v1/products/bulk
Content-Type: application/json
[
{
"name": "Monitor arm",
"price": 59,
"sku": "MA-004"
},
{
"name": "Webcam",
"price": 49,
"sku": "WC-005"
}
]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:
| Request | Status | Message |
|---|---|---|
| A body that is not an array, or an empty array | 400 | Send a non-empty JSON array. |
More items than maxBulkSize | 400 | You can create at most 50 items at once., with the limit of the route in place of 50 |
A body larger than the bodyLimit option | 413 | Request body is too large. |
A request with 51 items against a limit of 50 answers like this:
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"
}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:
POST /api/v1/products/bulk
Content-Type: application/json
[
{
"name": "Monitor arm",
"price": 59,
"sku": "MA-004"
},
{
"name": "",
"price": -1
}
]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:
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"
}
]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:
GET /api/v1/productsHTTP/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
}
}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:
POST /api/v1/notes/bulk
Content-Type: application/json
[
{
"title": "Shopping list"
},
{
"title": "Packing list"
}
]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
| Status | code | When it happens | What to change |
|---|---|---|---|
| 400 | BAD_REQUEST | The body is not an array, the array is empty, or it holds more than maxBulkSize items | Send between 1 and maxBulkSize items |
| 401 | ACCESS_TOKEN_MISSING | The route needs sign-in and no token or cookie was sent | Sign in first |
| 403 | ROLE_NOT_ALLOWED | The route lists roles and the caller's role is not among them | Sign in with an allowed role |
| 409 | DUPLICATE_KEY | An item repeats a value of a unique field, in the request or in the collection | Send unique values, and check what was saved |
| 413 | PAYLOAD_TOO_LARGE | The body is larger than bodyLimit | Send fewer items, or raise bodyLimit |
| 422 | VALIDATION_FAILED | One or more items break the validation group | Fix 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.
:::