Validate SvelteKit endpoints with Joi
Tags: svelte, webdev, sveltekit, joi
Published: 2022-10-02 12:24:04

## 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:
```javascript
if(!username || username.length < 3 || username.length > 30) {
  // return error
}
```
But instead I am gonna use [Joi](https://joi.dev) for validations.

Joi validator used to be part of [hapi](https://hapi.dev/) 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:
```javascript
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`

```html
<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:

![Form](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dbksynmzbg9h2ee620lh.png)

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:
```typescript
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`

```typescript
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:

1. log
2. authenticate
3. validate

Works very similar to Express's middleware. Only on SvelteKit it's called sequence. Here's how our hook sequence can look like:

```typescript
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:

```typescript
import entrySchema from './routes/api/entry/validation';

const map: { [keys: string]: any } = {
  '/api/entry': entrySchema
};
```

where `./routes/api/entry/validation` is:

```typescript
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:
1. get current path and method
2. check if map object has a key with current path
3. check if map object has validation for current method
4. run the validation
5. if validation has an error then return 400 error
6. if validation has no error then handover to an endpoint


```typescript
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:

```typescript
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](https://github.com/Ascarbek/sveltekit-joi-example)

Feel free to leave any comments.

Cheers!