Create
data is always an array, even for a single record — the response shape never has to differ between creating one record and creating a hundred. Only the fields the blueprint declares go in each entry. id, tenantId, ownerId and the timestamps are never accepted from the client — they are the server’s to decide, and sending them has no effect. See Details for exactly which fields are required, straight from the resource’s own contract.
POST /blog-posts/create
Authorization: Bearer <token>
Content-Type: application/json
{
"data": [
{
"title": "Hello world"
}
]
}Body fields
Section titled “Body fields”| Field | Type | What it does |
|---|---|---|
data |
array | Each entry: the fields the blueprint declares as creatable, nothing else |
Each entry in the response reports its own status — one entry failing validation never blocks the others in the same request. A failed entry carries error instead of data.
When the payload is rejected
Section titled “When the payload is rejected”A payload that fails Zod’s own schema — a missing required field, for instance — never reaches the controller: ZodValidationPipe throws before the handler runs, so the response is a plain 400, not the domain error envelope every other rejection in this framework uses. This is a real gap, not a documented choice — see the “Payload invalide” scenario above for exactly what comes back today.
The real generated handler, from examples/blog-post/blog-post.controller.ts — every resource gets the same shape, only the field list changes:
@Post('create')
@Capability(canCreateBlogPost)
async create(
@Req() req: RequestWithUser,
@Body(new ZodValidationPipe(createBlogPostRequestSchema))
body: CreateBlogPostRequestBody,
) {
const subject = subjectOf(req.user);
const entries = body.data.map((item, index): BatchEntry<BlogPostView> => ({
index,
run: async () =>
toBlogPostView(await this.blogPosts.create(subject, item)),
}));
const { results, meta } = await this.runBatch(entries, body.atomic);
return ok(results, meta);
}