Logging in
Now we have a UI, we can wire it up. The first step is to create some logic on the server that can handle form submissions. In SvelteKit, we have a feature for this called remote functions.
Create a new file called lib/auth.remote.ts. That .remote suffix tells SvelteKit that this module is special: you can import it wherever you like, but the logic will always run on the server, where you can do privileged things like reading cookies and interacting with databases.
import { form } from '$app/server';
export const login = form(async () => {
console.log('TODO implement login');
}); We can now spread login onto our form:
<script lang="ts">
import logo from '$lib/assets/logo.svg';
+ import { login } from '$lib/auth/index.remote';
</script>
<div class="splash">
+ <form {...login}>...</form>
</div> Watch the terminal with your dev server, and click the button.
We need to get the handle from the browser to the server, and here’s where things get a little dangerous. A remote function isn’t just a function that you can call internally, it’s a public HTTP endpoint.
So when you accept data from the internet, you need to check what kind of data it is — you can’t just trust that it’s your client code calling your server code.
The word for that is validation, and we’re very lucky that there’s actually an industry standard for this, called Standard Schema.
There’s a bunch of Standard Schema libraries. Zod is the best known one. The one that I tend to use, and which is popular in the Svelte ecosystem, is Valibot.
Install it in the usual way:
pnpm add valibot Then, add it to your login form:
import { form } from '$app/server';
import * as v from 'valibot';
export const login = form(
v.object({
handle: v.pipe(v.string(), v.nonEmpty('Handle cannot be empty'))
}),
async (data) => {
console.log(data);
}
); If we click the button now… nothing happens! But check your devtools. The validator is working.
We need to add the handle field to the <input>:
<input class="rounded" {...login.fields.handle.as('text')} /> Now, the data gets sent to the server. But what if the data is still invalid?
First, we can display the validation error on the page:
<label>
Sign in
+ {#each login.fields.handle.issues() as issue}
+ <p role="alert">{issue.message}</p>
+ {/each}
<input class="rounded" {...login.fields.handle.as('text')} />
</label> Secondly, the element itself will receive an aria-invalid="true" attribute, which we can use for styling:
input {
display: block;
width: 100%;
margin: 0.5rem 0;
+ &[aria-invalid='true']:not(:focus) {
+ outline: 2px solid red;
+ }
}
+[role='alert'] {
+ color: red;
+} And here comes the party trick: try disabling JavaScript and interacting with the form.