Remote Functions: form validation #14288
Replies: 33 comments 104 replies
-
I tried solving this in a pet project by having a base schema (shareable between server and client), then extending it for a client-specific schema and a server-specific schema. The server schema would, like you said, make database calls, while the client schema would send requests to the server (like checking if a username is taken before we submit the registration form). I wonder if there's some magic that can happen here where we add a query remote function inside a schema and that just becomes a function call on the server but a fetch in the browser. |
Beta Was this translation helpful? Give feedback.
-
This is exactly why Svelte keeps showing who’s boss in the market. While other frameworks drown developers in boilerplate, SvelteKit is pushing forward with native, typed, and integrated form validation that solves a real DX pain point cleanly. Turning FormData into typed objects, supporting arrays and nested properties out of the box, and returning structured issues and inputs is nothing short of a game changer. Once again, Svelte proves it’s one step ahead: simple on the surface, powerful underneath. |
Beta Was this translation helpful? Give feedback.
-
I don't think this is a dealbreaker for me in particular, but the success of superforms indicates that it's an important feature for many people. I don't think My question is: which is more valuable, the ability to put server-specific validations inside your schema, or the UX improvement of instant client validation? I think the clear answer is the latter. Server-side validation pros:
Cons:
Universal validation pros:
Cons:
|
Beta Was this translation helpful? Give feedback.
-
Maybe this could solve confidential data being returned in export const myForm = form(
z.object({ password: z.string() }),
() =>{},
{
protectedInputs: ['password']
}
); |
Beta Was this translation helpful? Give feedback.
-
I really want a type-safe const { form, name }: {
form: TForm,
name: FormInputName<TForm>
} = $props(); |
Beta Was this translation helpful? Give feedback.
-
I do think there needs to be a way to list just the root-level issues somewhere, if I'm already displaying the field-level issues next to each field. |
Beta Was this translation helpful? Give feedback.
-
I'm using this small lib for this: https://github.com/fabian-hiller/decode-formdata. Maybe it can help as inspiration (or just use it directly) |
Beta Was this translation helpful? Give feedback.
-
This would be awesome! This is one of the best things about Angular that is is unique: Angular Forms. However, instead of reinventing validation, you allow Zod, Valibot, etc. So, my 2c would be for this to work both in Server Actions and in vanilla Svelte. J |
Beta Was this translation helpful? Give feedback.
-
Would love to see both server and client side validation!! |
Beta Was this translation helpful? Give feedback.
-
I hold to my core sincerity for the enormous amount of effort given to svelte — pushing the boundaries is what keeps this framework evolving. 👏 Let's 👏 go! My ultimate fear is violation of the principles Svelte promises: the values that have defined its identity and trust with the community. These include (some are verbatim):
The loser in this equation could be measured in the impact compromise has overall, in the long run. That said, baking form validation directly into Once SvelteKit ships with a parsing + Standard Schema pipeline (or similar), it’s reasonable for developers to assume:
That’s a large, ongoing surface area for a framework built on small, predictable primitives and an ethos of:
The proposal cuts across those lines by:
If we must, an idiomatic path forward could be that we keep This would preserve SvelteKit’s "small core, powerful primitives" DNA, avoids inheriting every parsing/validation edge case, addresses the assumptions problem, and still makes the happy path easy for those who opt in. ✌️ |
Beta Was this translation helpful? Give feedback.
-
Overall, looks good! Opt outTo opt out, I assume if you omit the schema argument, the callback you passed to Client-side validationNo client-side validation feels like a very bad downside. I get that some people want to wrap database calls inside their validation (which I think conflates two different things), but being able to do basic data validation in the form while the user is filling it out is what I would prefer. It's much nicer to be told about an error immediately as opposed to filling out the entire form, scrolling down, hitting submit, and then seeing errors. The manual |
Beta Was this translation helpful? Give feedback.
-
What would the api look like for populating a form with data? would inputs just be a <script lang="ts">
updateUser.inputs = await getCurrentUser();
</script>
<form {...updateUser}
<label>
Username:
<input name="username" bind:value={updateUser.inputs?.username} />
</label>
...
</form> |
Beta Was this translation helpful? Give feedback.
-
I was the person who set off the thread this discussion spun out from. I'm stuck using a third-party library to do client-side validation. As much as I'd like to rely on server-only validation, designers request experiences that require client-side validation. It's a deal breaker not to have client-side validation, and it was the impetus for my original comment. I need to be able to revalidate input, on input, and I need to manipulate the UI (button states, etc.) based on the validity of the form. |
Beta Was this translation helpful? Give feedback.
-
Not a dealbreaker if it's server-side only. In fact, feeling very guilty to have this much good DX. |
Beta Was this translation helpful? Give feedback.
-
Hi there! I'm the maintainer of formgator, a
|
Beta Was this translation helpful? Give feedback.
-
This package enables client side validation for remote functions. It's interesting what people already come up with. Maybe this can inform some decisions for the official implementation. |
Beta Was this translation helpful? Give feedback.
-
I already have an entity responsible for the state of the form on the client (
Now you cannot edit async function handle_submit(form, form_data, callback) {
const data = convert_formdata(form_data);
const validated = await preflight_schema?.['~standard'].validate(data);
if (validated?.issues) {
issues = flatten_issues(validated.issues);
return;
} I also have an initialized validator that I would like to run before submitting the form
Compared to the backend version (with This is not a problem at all, but I planned to use |
Beta Was this translation helpful? Give feedback.
-
I noticed the docs were recently update to include the new form schema validation. This is awesome! Is there any word on the manual validation side of things and the proposed For example, my setup below feels a bit awkward. I'm manually returning a few issues that don't naturally fit into schema validation and it feels clunky for my component to use export const signup = form(signupSchema, async ({ email, username, password }) => {
const signupEnabled = await signupEnabled();
// manual form level issue not specific to any one field
if (!signupEnabled) {
return {
issues: { $form: 'Signup is disabled' }
}
}
const existingProfile = await getUserProfileByUsername(username);
// manual issue specific to the username field
if (existingProfile) {
return {
issues: { username: 'Username is taken' }
};
}
//...
}) |
Beta Was this translation helpful? Give feedback.
-
What is the recommended way to handle prefilled values? Setting the value prop on an input element doesn't seem to populate the myForm.input object; it only gets filled when you change the input value (which I assume is the expected behaviour). And setting the values directly via the myForm.input e.g. So e.g. this would display the input value but not the 'Current value:'
I guess a current workaround would be to use the initial data as fallback value: |
Beta Was this translation helpful? Give feedback.
-
In case any one is not aware, there is a different thread talking about how the current implementation of While I am definitely pro normal schemas, it opens up great questions I think should be considered on the right way to handle it. In my mind there is no point in having a schema that cannot work out of the box, so it should work as expected, or be removed completely from the Svelte is its own language, and I prefer anything features that save me time. Handling form data is something any complex component needs to simplify without the same boilerplate over and over. J |
Beta Was this translation helpful? Give feedback.
-
Have you considered exporting the function that does <!-- +page.svelte -->
<script lang="ts">
import { turnFormDataIntoPojo } from 'svelte'
import { queryData } from '...remote.ts'
let filters = $derived(turnFormDataIntoPojo(page.url.searchParams))
let data = $derived(queryData(filters))
</script>
<form>
<input type="checkbox" name="topic[]" value="Foo" >
<input type="checkbox" name="topic[]" value="Bar" >
<input type="checkbox" name="topic[]" value="Baz" >
<button type="submit">Filter</button>
</form>
|
Beta Was this translation helpful? Give feedback.
-
Sorry to ask, was not sure if this was answered, but will there be a way to use client-side validation without importing from a remote function? Seems like a shame to have built-in client validation from a framework and only tie it to Remote Functions. |
Beta Was this translation helpful? Give feedback.
-
What Rich is talking about is the usual boilerplate that other frameworks use: function submit(event: SubmitEvent) {
const target = event.target as HTMLFormElement;
const form = new FormData(target);
const data = Object.fromEntries(form);
... Maybe this boilerplate could be extracted out, and we could find a way to attach the schema to a rune so that you could easily run |
Beta Was this translation helpful? Give feedback.
-
Form validation as greatly simplified some boileplate in the tests I wrote a couple of weeks ago to test remote functions. Great addition. So first, thank you for adding this. |
Beta Was this translation helpful? Give feedback.
-
Hi all! I’m trying to use Drizzle ORM and Valibot together with SvelteKit’s new form validation API. Ideally, I’d love to do something like this: // schema.ts
import { integer, pgTable, text } from 'drizzle-orm/pg-core';
import { createInsertSchema } from 'drizzle-valibot';
export const blogTable = pgTable('blog', {
id: integer().generatedAlwaysAsIdentity().primaryKey(),
title: text('title').notNull(),
content: text('content').notNull(),
});
export const blogInsertSchema = createInsertSchema(blogTable, {}); // auth.remote.ts
import { form } from "$app/server";
import { db } from "$lib/server/db";
import { blogTable, blogInsertSchema } from "$lib/server/db/schema";
import { unstable_coerceFormValue as coerceFormValue } from '@conform-to/valibot';
export const createBlogPost = form(
coerceFormValue(blogInsertSchema),
async (data) => {
await db.insert(blogTable).values(data);
}
); This doesn’t currently work out of the box, but I really like the semantics and would love to avoid duplicating validation logic. Is there any way to achieve something similar, or any recommended approach for integrating Drizzle/Valibot schemas with SvelteKit forms? Any advice or pointers would be greatly appreciated! |
Beta Was this translation helpful? Give feedback.
-
If anyone knows how to create Remote functions that can accept arguments like the following, please let me know 🙏 // chatForm.remote.ts
const getSchema = (min: number) => v.object({
text: v.pipe(
v.string(),
v.minLength(min)
)
})
export const chatForm = (min: number) => form(getSchema(min), async (data) => {
// …
}); <form {...chatForm(3)}>
<button type="submit">Submit</button>
</form> |
Beta Was this translation helpful? Give feedback.
-
I created a util for mapping FormData to (mildly?) complex objects -- i'll admit that some of the code is not great quality, but should serve as a reference point. https://github.com/jhechtf/mono/blob/main/packages/arktype-utils/src/formData.ts#L166 I bring it up because recently I tried to use the new form validation available, and even though my forms previously worked with actions, I was running into issues when trying to use the form remote function. |
Beta Was this translation helpful? Give feedback.
-
Quick question about using Is there a way to just populate the issues fields without creating an error? Currently invalid produces an error which is caught inside of a try catch. This can be problematic if you have some function that has multiple points of failure that may not always be related to form validation. Lets say I have some login function // pseudo-ish code thats simplified to hopefully provide context of what i am talking about
const loginSchema = z.object({
email: z.string(),
password: z.string()
})
const login = form(loginSchema, async (data, invalid) => {
try {
const user = await db.user.findUnique({where:{email: data.email}})
if(!user) invalid(invalid.email("No account found!"))
const passwordMatch = compare(user.hashedPassword, data.password)
if(!passwordsMatch) invalid(invalid.password("invalid password!"))
someMaybeFailFn()
redirect(303, "/account")
} catch (e){
// if i called invalid in the try block it is now caught here
// some check here to see if (and why) invalid was called and redo it ( what i want to avoid having to do )
checkIfIsBecauseOfInvalidAndRethrow(e)
// would have to do these checks anyways even if invalid was not used in the try block so no harm no foul
if (e instanceOf PrismaKnownRequestError) { invalid("Prisma error <message>)}
else if (e instanceOf someOtherErrorType) {invalid("some other error happened!")}
else { invalid("Internal server error!")} // dunno why the error happened so just throw a 500
}) Fully admit this may be a skill issue however I am just hoping to avoid having all of my invalid calls being caught and then having to be rechecked in the catch block if possible |
Beta Was this translation helpful? Give feedback.
-
Hi everyone, Building on the conversation around live, client-side validation for remote functions, I did a deep dive into the current TL;DR: form.validate() successfully prevents network requests when validation fails, but makes a server validation request on every keystroke that produces valid data. This makes it unsuitable for live validation UX patterns. The User Experience GapHere's what happens today when typing into a form with form.validate() called on oninput:
This creates unnecessary server load and doesn't match the expected behavior for live validation, where the goal is to give instant feedback without server round-trips once the client schema is satisfied. Why This HappensI traced this to the implementation logic in the source code: client/remote-functions/form.svelte.js#L588 const validated = await preflight_schema?.['~standard'].validate(convert(form_data));
if (validated?.issues) {
// Branch taken when validation FAILS
array = validated.issues;
} else {
// Branch taken when validation SUCCEEDS
// because the `issues` property is undefined
form_data.set('sveltekit:validate_only', 'true');
const response = await fetch(/* ... */);
} The logic pivots on the presence of Proof with Valibot: import * as v from 'valibot';
const commentSchema = v.object({
content: v.pipe(v.string(), v.minLength(10))
});
const fakeFormData = new FormData();
fakeFormData.append('content', 'This is a valid string');
const data = Object.fromEntries(fakeFormData.entries());
console.log(commentSchema['~standard'].validate(data));
// Logs:
// {
// "value": { "content": "This is a valid string" },
// "typed": true
// }
// Note: No `issues` property exists on successful validation This confirms that every valid state triggers a network request, even when we only want client-side validation feedback. I understand the design rationale: preflight validation is meant to preflight actual submissions, catching errors before they hit the server. The server validation call with This is perfect for pre-submission validation (validating once before submit), but it doesn't serve the live validation use case (validating on every keystroke without server calls). The Feature RequestWhat's needed is a way to tell <form {...commentForm.clientValidation(commentSchema)}> Behavior:
Current Workaround (Not Recommended)For reference, here's the hacky solution I'm using now: <script>
import { commentForm } from './commentForm.remote.js';
import * as v from 'valibot';
const BLOCKED = 'blocked';
const OPEN = 'open';
const commentSchema = v.object({
blockState: v.literal(OPEN), // Prevents auto-submit
content: v.pipe(v.string(), v.minLength(10))
});
let blockState = $state(BLOCKED);
async function unlockAndSubmit() {
blockState = OPEN;
document.getElementById('commentForm')?.requestSubmit();
setTimeout(() => { blockState = BLOCKED; }, 200);
}
</script>
<form id="commentForm" {...commentForm.preflight(commentSchema)}
oninput={() => commentForm.validate()}
>
<input name="blockState" value={blockState} type="hidden" />
<textarea name="content" rows="4"></textarea>
<button onclick={unlockAndSubmit}>Save</button>
</form>
{#each (commentForm.fields.allIssues() ?? [])
.filter(issue => !issue.message.includes(`Expected "${OPEN}"`)) as issue}
<div>{issue.message}</div>
{/each} This works but requires hidden fields, timing hacks, and message filtering. SummaryThe current To support live validation properly, we need an explicit API that:
Hope this analysis helps! |
Beta Was this translation helpful? Give feedback.
-
After successfully implementing manual client-side validation, I immediately ran into the next logical challenge: the interaction between live client-side errors and post-submission server-side errors. After some experimentation, I've landed on a state management logic that feels very intuitive from a user's perspective. I wanted to share it here as it highlights a crucial aspect that any official API for this feature would need to address. My Proposed Model: "Stale Server Error Invalidation"
State Flow Definition
Hope this is helpful for the design process |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
If you're passing arguments to
query
andcommand
remote functions, you're required to provide a Standard Schema for validation, but no validation is required for aform
.This caused many of you — justifiably — to raise eyebrows. The rationale was that the type of the argument is always going to be a
FormData
object, and you can't type the contents of aFormData
object (it's not generic), so you're going to have to do the validation inside the callback anyway. But it's clearly not ideal.Following on from a lengthy conversation on the original discussion, this is a proposal to make form validation built-in.
Conversion
The first step is to convert the data to an object that can be validated. In the basic case...
...this is easy — just create a
{ username: string, password: string }
object. What makes it a little trickier is that fields can be repeated......in which case you need to call
data.getAll('modifications')
on the resultingFormData
object. The wrinkle is that it's not possible to know, from the server's perspective, whethermodifications
is supposed to be astring
or astring[]
in the case that there's only one checked value.We can side-step that problem by disallowing duplicate fields and using syntax to denote arrays:
On the server, we can 'hydrate' these into arrays:
form.issues
andform.input
Armed with this, we can validate your data and report issues back to the client, along with the submitted data, in a structured way:
You could of course build opinionated abstractions around this:
Reactivity
myForm.input
would be updated oninput
events, allowing you to express relationships between form controls:Types
issues
is aRecord<string, Issue[]>
whereIssue
comes from Standard Schema. Each issue has amessage
and apath
property.input
is aRecord<string, FormDataEntryValue>
. (Not 100% sure yet what would happen in the case of an<input type="file">
.)Rolling up issues
Issues belonging to members of arrays are rolled up to the arrays themselves — in other words, to take our earlier food ordering example,
issues.modifications
would include any issues belonging tomodifications[0]
, etc. Theissue.path
can be used to distinguish between them.issues.$
could be a place to put root-level issues, and a rollup of all the issues found while validating.Manual validation
Since validators can be async functions, and since you can use
getRequestEvent
inside those functions, it should be possible to do the bulk of your checking inside your schema. As a bonus, if you have multiple async checks they will happen in parallel.There are some cases where you don't know if data is valid until you attempt to perform some action. In these cases, you might still want to report a validation error (populating
issues
andinput
) rather than returning or erroring. For that reason, we will probably need to provide a method for populatingissues
programmatically:invalid
would be typed such that you could only use keys matching the schema. It throws an understood-by-SvelteKit error, so as not to pollute the type of the return value — in other words, returning always indicates success. A non-invalid
error would cause the error page to appear, and thus should be avoided.Client-side validation
Validation is server-first, but you can add client-side preflight validation, and you can trigger validation programmatically to populate
issues
before the form is submitted.Preflight validation, like server validation, accepts a Standard Schema:
Now, before the form is submitted to the server, it will be checked against the preflight schema. If it fails to validate,
login.issues
will be populated. If it does validate, it will then be submitted so that you can validate the data on the server. This may involve the same schema (exported from a shared module, since you cannot export a schema from a.remote.ts
file — you can only export remote functions), but in many cases the server-side validation will include additional checks.Separately, you can programmatically validate the form on demand with the
validate
method. You can call this whenever you like, for example after an input is blurred:This will validate the data and populate
login.issues
, but it won't submit. By default, issues relating to controls that aren't yet dirty (i.e., they haven't been interacted with) would be omitted.Wrinkles
login.validate
? That would allow us to give immediate feedback, but it would also prevent server validation from occurring if the form was incomplete — for example in a registration form we wouldn't be able to check ifusername
was already in use untilpassword
had already been filled out.preflight
andvalidate
APIs to work, we need a 1:1 correspondence between<form>
and form function. (<form>
follows function, heh.) This is easy enough to implement with an attachment, and if you need multiple instances you can useform.for
(which we need to get around to documenting), but are there situations in which this would be restrictive, perhaps aroundbuttonProps
?Security
So far this proposal assumes that it's safe to return submitted data back to the user in case of a validation error. This gets slightly murky around things like passwords and credit card info.
The conventional wisdom on this seems to be that it is safe — if you trust everything between the browser and the server (which you have to in order to do anything) then it stands to reason that you can trust everything between the server and the browser.
But it deserves careful scrutiny. Does having your password in the HTML of the response (even if in a JSON object rather than the markup) create vulnerabilities that aren't already present? (Obviously if a bad actor has access to your computer, they can get your password from the HTML, but they could already have installed a keylogger or inspected
input.value
, so that's not a new vulnerability.) Given that POST requests aren't cached, I would like to imagine it doesn't. I hope that the AI companies currently threatening us with new browsers, so that they can crawl more content without having to circumvent anti-bot measures, are careful enough not to add POST responses to their training data.It's probably fine, but I'd love for people to share their thoughts on this!
Beta Was this translation helpful? Give feedback.
All reactions