Your form has two paths. The happy one β valid input, green checkmark β gets tested
every time you touch the page. The sad one β the 422, the per-field error
messages, the red text under the input β usually can't be tested at all, because every
free mock API accepts any payload. Send {"price": "cheap"}
to JSONPlaceholder or DummyJSON and they cheerfully return 201 (checked July
2026); json-server stores whatever you send, by design. So your error-rendering code ships unexercised, and
the first person to see it work β or not β is a real user hitting the real backend.
Mockbird writes can validate against the schema you already defined. Free, no signup, works on the public demo right now.
# price is a number field β send a string, get a proper 422
curl -X POST 'https://mockbird.mockbird.workers.dev/m/demo/products?mock_validate=1' \
-H 'content-type: application/json' \
-d '{"name":"Widget","price":"cheap"}'
{
"error": "validation_failed",
"fields": {
"price": "expected number, got string"
}
}
HTTP status 422, one entry per bad field, human-readable reasons. Send
several bad fields and you get all of them at once β exactly what your form needs to
render every red message in one round-trip:
curl -X POST 'https://mockbird.mockbird.workers.dev/m/demo/products?mock_validate=1' \
-H 'content-type: application/json' \
-d '{"name":"Widget","price":"cheap","inStock":"yes","rating":"high"}'
# β 422 {"error":"validation_failed","fields":{
# "price":"expected number, got string",
# "inStock":"expected boolean, got string",
# "rating":"expected number, got string"}}
Validation runs against the field types of your resource schema β the same schema that seeds your fake data:
| Field type | Rule | Example failure |
|---|---|---|
number, price, rating, percent, age, refId⦠| must be a JSON number | "expected number, got string" |
boolean | must be true/false | "expected boolean, got string" |
email | well-formed address | "must be a valid email address" |
uuid | RFC-4122 shape | "must be a valid UUID" |
url, image, avatar | parseable URL | "must be a valid URL" |
ip | valid IPv4 | "must be a valid IPv4 address" |
date, pastDate, futureDate | parseable date string | "must be a parseable date string (ISO 8601 recommended)" |
oneOf (enums) | value must be in the list | "must be one of: active, trial, churned" |
| other string types | must be a string | "expected string, got number" |
Format checks work anywhere in the schema β the demo's customers resource
has an email field:
curl -X POST 'https://mockbird.mockbird.workers.dev/m/demo/customers?mock_validate=1' \
-H 'content-type: application/json' \
-d '{"firstName":"Ada","email":"not-an-email"}'
# β 422 {"error":"validation_failed","fields":{"email":"must be a valid email address"}}
?mock_validate=1 type-checks the fields you send and ignores the rest.
?mock_validate=strict behaves like a picky backend: unknown fields are
rejected, and every schema field is required on POST/PUT:
curl -X POST 'https://mockbird.mockbird.workers.dev/m/demo/products?mock_validate=strict' \
-H 'content-type: application/json' \
-d '{"name":"Widget","price":9.99,"color":"red"}'
# β 422 {"error":"validation_failed","fields":{
# "color":"unknown field (strict mode rejects fields not in the schema)",
# "description":"required", "image":"required",
# "category":"required", "inStock":"required", "rating":"required"}}
PATCH stays partial in both modes β only the fields you send are checked,
nothing is "required". That matches how real PATCH endpoints behave, so your inline-edit
UI can be tested honestly:
# partial PATCH with one valid field β 200, merged
curl -X PATCH 'https://mockbird.mockbird.workers.dev/m/demo/products/1?mock_validate=strict' \
-H 'content-type: application/json' -d '{"price":12.5}'
# partial PATCH with a bad type β still 422
curl -X PATCH 'https://mockbird.mockbird.workers.dev/m/demo/products/1?mock_validate=1' \
-H 'content-type: application/json' -d '{"price":"nope"}'
Query params are great for curl experiments, but your app's fetch layer shouldn't need to know it's talking to a mock. Set validation once, project-wide:
curl -X PUT https://mockbird.mockbird.workers.dev/api/projects/PROJECT/settings \
-H 'x-admin-key: KEY' -H 'content-type: application/json' \
-d '{"validate":"strict"}'
From then on every plain POST/PUT/PATCH against
the project validates β your normal form submits hit a rejecting API with no code changes.
Override per request with ?mock_validate=off (handy for seeding scripts);
clear the default with {"validate":"off"}. Also settable in the dashboard.
The response shape is fixed β error is always
"validation_failed", fields maps field name β reason β so you
can write your form's error plumbing against it once:
const res = await fetch(`${API}/products`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(formValues),
});
if (res.status === 422) {
const { fields } = await res.json();
for (const [name, message] of Object.entries(fields)) {
setFieldError(name, message); // your framework's per-field error setter
}
return;
}
if (!res.ok) return setFormError(`Something went wrong (${res.status})`);
celebrate(await res.json());
When the real backend arrives, keep the same plumbing and adjust the field-mapping line to its error format β the hard part (wiring errors to inputs, focus management, aria-invalid) is already built and tested.
Validation composes with the other simulation params, so one mock URL can exercise every failure your form will ever meet:
| Scenario | How |
|---|---|
| Bad payload β 422 field errors | ?mock_validate=1 (this guide) |
| Server blows up β 500 | ?mock_status=500 (takes precedence over validation β a forced status wins) |
| Slow submit β double-click protection | ?mock_delay=3000 β is your button disabled while pending? |
| Flaky network β retry logic | ?mock_chaos=0.3 fails 30% of requests with 5xx/429 |
| Rate limited β 429 + Retry-After | ?mock_ratelimit=5 (5 requests/60s per client) |
| Session expired β 401 | ?mock_status=401, or real mock JWTs with 5-second expiry |
More recipes in Testing loading and error states.
This is type and format validation, not business rules: there are
no min/max ranges, no cross-field checks ("end date after start date"), no async
uniqueness. It covers the shape of the sad path β which is what your form's error
rendering needs β not your domain logic. A non-object body (a bare JSON array, or invalid
JSON) is a 400 {"error":"invalid JSON body"}, not a 422. And credit where
due: Prism does richer,
OpenAPI-spec-driven request validation β but it runs on your machine and needs you to
maintain a spec; Mockbird is a hosted URL and the schema is the one your resources
already have. If you have a spec, import it and you get both.
curl -s -X POST https://mockbird.mockbird.workers.dev/api/projects \
-H 'content-type: application/json' \
-d '{"name":"my-api","preset":"ecommerce"}'
β¦or the one-click dashboard link. Enum fields
validate too β define a field with
{"type":"oneOf","values":["active","trial","churned"]}, or
import an OpenAPI spec and its enums are kept verbatim.
10,000 requests/day free, plus GraphQL,
snapshots and a
request inspector to see exactly what your form sent.