Validation errors
A rejected write comes back as one HTTP 400 body with a field error per Zod issue, and a hook can raise the same shape for rules a single field schema cannot see.
Your collection rejected a write. This page is what the caller sees, how the typed client reads it, and how you raise a failure of your own.
The body
A failed parse becomes an ApiError with code VALIDATION_ERROR, which maps to
HTTP 400. The response wraps it under an error key:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Validation failed",
"fieldErrors": [
{
"path": "settings.theme",
"message": "Invalid option: expected one of \"light\"|\"dark\""
}
]
}
}| Key | What it holds |
|---|---|
code | VALIDATION_ERROR from a schema parse, BAD_REQUEST from ApiError.badRequest. |
message | A summary line. QUESTPIE translates it when the error carries a translation key. |
fieldErrors | One entry per Zod issue, each with a dotted path and its own message. |
cause | The raw Zod issues as a string. stack joins it in development. |
Nothing is written when the parse fails. No row, no beforeChange, no
afterChange.
Reading it in the client
The typed client throws QuestpieClientError on any non-2xx response and parses
the envelope for you.
import { QuestpieClientError } from "questpie/client";
try {
await client.collections.product.create({ name: "Chair", sku: "sku-1" });
} catch (error) {
if (
error instanceof QuestpieClientError &&
error.isCode("VALIDATION_ERROR")
) {
error.getFieldError("sku"); // { path: "sku", message: "Use SKU-000000" }
error.getFieldErrorsMap(); // { sku: ["Use SKU-000000"] }
}
}The admin form does the same thing and sets each message on its own input, so a rule you wrote in the collection file shows up under the right control with no extra work.
Rules a field schema cannot see
A field schema sees one field. When the rule spans two, put it in
beforeChange and throw ApiError.badRequest. It carries the same
fieldErrors shape and the same HTTP 400.
import { collection } from "#questpie/factories";
import { ApiError } from "questpie/errors";
export default collection("booking")
.fields(({ f }) => ({
startsAt: f.datetime().required(),
endsAt: f.datetime().required(),
}))
.hooks({
beforeChange: ({ data }) => {
if (data.startsAt && data.endsAt && data.endsAt <= data.startsAt) {
throw ApiError.badRequest("End must be after start", [
{ path: "endsAt", message: "Must be after startsAt" },
]);
}
},
})
.title(({ f }) => f.startsAt);The caller now gets code: "BAD_REQUEST" and a fieldErrors entry pointing at
endsAt, which the admin renders under that input like any other failure.
Guard the fields you compare
beforeChange runs on updates too, where a patch may carry one date and not
the other. Check both are present before comparing them.
Related
- Validation, the schemas that produce these errors and the three ways to shape them.
- Hooks, where
beforeChangesits and what itsctxcarries. - Client SDK, the rest of
QuestpieClientError.
Transition hooks
A workflow stage move is not a write, so the two hooks around it carry their own context, and only the first of them can stop the move.
Retention
A retention job is application code you write around purgeById. QUESTPIE supplies the index it pages through and the irreversible unit it calls, and nothing else.