QUESTPIE
SchemaValidation

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.

View markdown

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\""
			}
		]
	}
}
KeyWhat it holds
codeVALIDATION_ERROR from a schema parse, BAD_REQUEST from ApiError.badRequest.
messageA summary line. QUESTPIE translates it when the error carries a translation key.
fieldErrorsOne entry per Zod issue, each with a dotted path and its own message.
causeThe 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.

src/questpie/server/collections/booking.ts
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.

  • Validation, the schemas that produce these errors and the three ways to shape them.
  • Hooks, where beforeChange sits and what its ctx carries.
  • Client SDK, the rest of QuestpieClientError.

On this page