A signup form that accepts bad input costs you a row in a database. A prompt form that accepts bad input costs you a paid API call, several seconds of the user staring at a spinner, and an output so generic they assume your product is broken. Validation stops being hygiene and starts being cost control.
The awkward part is that most validation habits transfer badly. Rules built for emails and postcodes assume the field is metadata about a person; here the field is the payload, and “well-formed” is not the same thing as “usable”. Below is what actually needs enforcing, with FormValidation doing the work.
Prompt forms break three normal assumptions
Worth naming these before writing any rules, because each one changes what you validate for:
- The free-text field is the product input, not a description of something else. A user can write forty perfectly grammatical characters that contain no usable instruction.
- The expensive operation happens after submit and cannot be rolled back on the client. Once the request goes out, you have spent the money whether the result is good or not.
- Minimums matter more than maximums. Most forms only care about upper bounds; prompt forms need a floor, because thin input is the single biggest cause of disappointing output.
Length limits that map to token budgets
Start with a floor and a ceiling on the main text field. Roughly four characters per token is close enough for client-side purposes — the point is not accounting precision, it is refusing input that cannot possibly work and input that will be truncated server-side anyway.
const fv = FormValidation.formValidation(
document.getElementById('promptForm'),
{
fields: {
premise: {
validators: {
notEmpty: {
message: 'Describe what you want written.',
},
stringLength: {
min: 40,
max: 1200,
message:
'Give 40 to 1,200 characters — thin prompts produce generic output.',
},
},
},
},
plugins: {
trigger: new FormValidation.plugins.Trigger(),
submitButton: new FormValidation.plugins.SubmitButton(),
message: new FormValidation.plugins.Message({
clazz: 'fv-help-block',
}),
},
}
);
The message text is doing real work here. “Invalid input” teaches the user nothing; naming the consequence of a thin prompt teaches them how the product behaves.
Structure the prompt into fields, then validate each one
One giant textarea is the worst option on both axes — worst for output quality, and almost impossible to validate meaningfully. The fix is to pull every constrainable parameter out of the prose and into its own field.
Consumer tools tend to arrive at this layout independently. An AI story generator will typically collect genre, tone, character names and a target length alongside the free-text premise, rather than asking for one paragraph and hoping the model infers the rest. From a validation standpoint that decomposition is the whole win: a select has a known option set, a target length has sane numeric bounds, and the free-text field shrinks to the part only the user can supply.
fields: {
genre: {
validators: {
notEmpty: {
message: 'Pick a genre so the model has a register.',
},
},
},
targetWords: {
validators: {
notEmpty: { message: 'Set a target length.' },
between: {
min: 300,
max: 5000,
message: 'Choose between 300 and 5,000 words.',
},
},
},
}
A second benefit shows up later: when a generation disappoints, structured fields give you something to change. A single blob of text gives the user nothing to adjust except vibes.
Run the cheap checks before the expensive one
Anything you can verify without calling the model should be verified first — remaining quota, plan limits, whether the account is even allowed this feature. An async validator keeps that logic inside the form lifecycle instead of scattered through a submit handler.
quota: {
validators: {
promise: {
message: 'No generations left on your plan today.',
promise: () =>
fetch('/api/quota')
.then((res) => res.json())
.then((data) => ({ valid: data.remaining > 0 })),
},
},
},
Two caveats. Debounce or trigger this on submit rather than on every keystroke, or you will DDoS your own endpoint. And treat a failed fetch as valid-but-unverified, letting the server be the authority — a flaky network should not silently block a paying user.
Own the waiting state
Generation takes seconds, not milliseconds, which means the gap between a valid form and a finished result is long enough for users to click again. Handle it explicitly:
- Disable the submit control the moment the form validates, and re-enable it only when the response resolves or errors.
- Show progress that is honest. A spinner with no text reads as a hang after about four seconds; a line of copy naming the step does not.
- Keep the inputs populated and editable. Users iterate on prompts, so a form that clears itself on submit is actively hostile.
- On a regenerate action, revalidate rather than assuming the previously valid state still holds — quota may have changed since the last call.
The last one catches a genuinely common bug: a form validated at 10:00 is not necessarily valid at 10:02 when the user hits regenerate for the fifth time.
Write messages like an editor, not a compiler
Because the input is creative, error copy carries more weight than usual. Users do not experience a rejected prompt as a validation failure; they experience it as the product refusing them. “Field is required” is technically accurate and completely useless. “Name at least one character so the story has someone to follow” tells them what to type next.
The same applies to soft guidance. A live character count with a note about what happens at the low end will prevent more bad submissions than any error message shown after the fact.
A short checklist
- Floor and ceiling on every free-text field, with the floor set where output quality actually falls apart.
- Every constrainable parameter moved out of prose and into its own validated field.
- Quota and permission checks resolved before the model call, failing open on network errors.
- Submit disabled during generation, inputs preserved, progress described in words.
- Revalidation on regenerate, not just on first submit.
- Error copy that names the next action instead of the broken rule.
None of this is exotic — it is the same validation discipline applied to a field where the cost of accepting garbage is measured in API spend and abandoned sessions rather than dirty data. That shift in stakes is the only thing that really changes.
Verify the validator and plugin names against the version you have installed before shipping; the API surface here has moved between major releases.