Askarbek Zadauly
Posted on October 2, 2022
Intro
In this post we are going to build a very simple fullstack app using SvelteKit and add an endpoint parameter validation to it. A lot of projects usually host their backend on a separate project serving from a subdomain. But SvelteKit can be used as a fullstack framework and it's easier to maintain one project rather than two.
Validation
It doesn't matter if you choose separate backend or all-in-one SvelteKit, you should have a backend validation for api endpoints as a safety measure in case if someone wants to send requests directly in order to break something. A straightforward way to do this would be just checking every parameter like this:
if(!username || username.length < 3 || username.length > 30) {
// return error
}
But instead I am gonna use Joi for validations.
Joi validator used to be part of hapi but then became a standalone library that you can use everywhere where validation is needed. So for example, here's how username validation can be done:
username: Joi.string().alphanum().min(3).max(30).required()
Use case
Let's build an App that has a form that user fills out and sends to our endpoint. If some of the parameters are invalid the endpoint will return an error message. If everything's fine then it will send it further to a server function to process.
Let's start
- First we create a scaffold project by running:
npm create svelte@latest sveltekit-joi-example
- Add tailwindcss for easier styling
- Then create our Page at
/src/routes/+page.svelte
<script lang="ts">
let formValues = {
username: '',
email: '',
password: '',
confirmPassword: '',
};
const onContinueClick = () => {
console.log('sending data: ', formValues);
}
</script>
<label>Username:</label>
<input bind:value="{formValues.username}" type="text" />
<label>Email:</label>
<input bind:value="{formValues.email}" type="text" />
<label>Password:</label>
<input bind:value="{formValues.password}" type="password" />
<label>Confirm password:</label>
<input bind:value="{formValues.confirmPassword}" type="password" />
<button on:click="{onContinueClick}">
Continue
</button>
After adding some tailwind magic I got form looking like this:
I will post a link to GitHub repo with full project.
Endpoints
Now it's time for a non-frontend stuff. SvelteKit has different ways to recognize backend code:
- naming files as
+server.ts
(or.js
) - putting files under
/src/lib/server
folder
Putting +server.ts files under /src/routes folder means you can call them using HTTP requests.
For example, let's say we have a file:
/src/routes/api/user/sign-in/+server.ts
That would mean we can call an HTTP request on this URL:
/api/user/sign-in
to execute code inside +server.ts
Which of HTTP request methods (GET, POST, PUT) can be called depends on exported function name:
export const GET: RequestHandler = async ({ request }) => {
// GET endpoint
}
export const POST: RequestHandler = async ({ request }) => {
// POST endpoint
}
All files under /src/lib/server
folder are server-only code. It's a good place for all DB interactions or external API calls.
So let's add our Joi validation into the endpoint:
/src/routes/api/entry/+server.ts
import type { RequestHandler } from '@sveltejs/kit';
import { json } from '@sveltejs/kit';
import Joi from 'joi';
import { SaveEntry } from '$lib/server/Entry';
const schema = Joi.object({
username: Joi.string().alphanum().min(3).max(30).required(),
password: Joi.string().pattern(new RegExp('^[a-zA-Z0-9]{3,30}$')),
confirmPassword: Joi.ref('password'),
email: Joi.string().email({ minDomainSegments: 2, tlds: { allow: ['com', 'net'] } }),
});
export const POST: RequestHandler = async ({ request }) => {
const params = await request.json();
const { error } = schema.validate(params);
if (error) {
let msg = '';
if (error.details.length && error.details[0].message) {
msg = error.details[0].message;
} else {
msg = 'unknown validation error';
}
return new Response(`Validation: ${msg}`, { status: 400 });
}
const { username, email, password, confirmPassword } = params;
await SaveEntry(username, email, password, confirmPassword);
return json({ success: true });
};
SaveEntry function is supposed to save to Database, but it's not in our scope right now so it's just an empty function.
Server Hooks
Validating params inside an endpoint is a straightforward approach. A better way would be to handle validations elsewhere so the endpoint code don't have to worry about it. Luckily SvelteKit has server hooks that can help with that. The hooks source file is located here:
/src/hooks.server.ts
The hooks are app-wide functions that SvelteKit calls in response to specific events. We are particularly interested in the handle
hook that gets called before endpoint functions on every HTTP request. The handle hook is basically a function but it can also be a chain of functions that will be called in sequence. A typical scenario for a hook sequence can be, for example:
- log
- authenticate
- validate
Works very similar to Express's middleware. Only on SvelteKit it's called sequence. Here's how our hook sequence can look like:
import { sequence } from '@sveltejs/kit/hooks';
import log from './sequences/log';
import auth from './sequences/auth';
import validation from './sequences/validation';
export const handle = sequence(log, auth, validation);
Now each request will be:
- Logged
- Checked for authorization token
- Validated if needed
Logging and Authorizing is out of our scope so those functions are empty for now. Now let's focus on the validation code.
We should be able to validate more than one endpoint. In order to do so we should have a Map (json object) where a key is an endpoint's path and a value is a Joi schema, like this:
import entrySchema from './routes/api/entry/validation';
const map: { [keys: string]: any } = {
'/api/entry': entrySchema
};
where ./routes/api/entry/validation
is:
import Joi from 'joi';
export default {
POST: Joi.object({
username: Joi.string().alphanum().min(3).max(30).required(),
password: Joi.string().pattern(new RegExp('^[a-zA-Z0-9]{3,30}$')),
confirmPassword: Joi.ref('password'),
email: Joi.string().email({ minDomainSegments: 2, tlds: { allow: ['com', 'net'] } }),
}),
};
As you can see I've added POST
key which would allow us to have different validations for POST
PUT
DELETE
requests.
Now let's move validation code into our hook. The logic is:
- get current path and method
- check if map object has a key with current path
- check if map object has validation for current method
- run the validation
- if validation has an error then return 400 error
- if validation has no error then handover to an endpoint
import type { Handle } from '@sveltejs/kit';
import entrySchema from '../routes/api/entry/validation';
const map: { [keys: string]: any } = {
'/api/entry': entrySchema,
};
const validation: Handle = async ({ event, resolve }) => {
for (const url of Object.keys(map)) {
if (event.url.pathname.indexOf(url) > -1) {
const method = event.request.method;
// we need to clone, otherwise SvelteKit will respond with 'disturbed' error
const req = event.request.clone();
const params = await req.json();
if (map[url][method]) {
const { error } = map[url][method].validate(params);
if (error) {
let msg = '';
if (error.details.length && error.details[0].message) {
msg = error.details[0].message;
} else {
msg = 'unknown validation error';
}
return new Response(`Validation: ${msg}`, { status: 400 });
}
}
}
}
return resolve(event);
};
export default validation;
NOTE: As you can see I'm calling event.request.clone()
to clone the request stream. This is a controversial approach. But if we don't call it then the endpoint function won't be able to get the params and SvelteKit will throw disturbed
error, whatever that means. Another approach would be to move params into event.locals.
Now we can remove validation from our endpoint, so the code is much cleaner now:
import type { RequestHandler } from '@sveltejs/kit';
import { json } from '@sveltejs/kit';
import { SaveEntry } from '$lib/server/Entry';
export const POST: RequestHandler = async ({ request }) => {
const params = await request.json();
const { username, email, password, confirmPassword } = params;
await SaveEntry(username, email, password, confirmPassword);
return json({ success: true });
};
In an endpoint function we are sure that all params are validated otherwise it wouldn't be called.
Form Actions
SvelteKit recommends to use form actions to handle requests and validations. While it's a valid approach but it doesn't cover all cases. You can checkout the project here:
https://github.com/Ascarbek/sveltekit-joi-example
Feel free to leave any comments.
Cheers!
Posted on October 2, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.