How to Exchange Data Between Client and Server Using SvelteKitAuth
In this article, we will explore how to exchange data between the client and server using SvelteKitAuth. We will delve into the importance of JWT and session callbacks, and how to synchronize and store sessions effectively.
Updating and Syncing data from the Client to the Server
Since the SvelteKitAuthConfig
is attached to the hooks, it triggers with every API call that is intercepted by the hooks. The SvelteKitAuthConfig
comes with two important callback functions, i.e., jwt
and session
, that trigger on every network request intercepted by the hooks.
The jwt
callback holds the token
property, which includes the authenticated user's token and profile data. This information constitutes the session information that will be shared site-wide. This information is stored in an encrypted format in the session-token
cookie.
On every network request, this callback decrypts the session-token
cookie, reads the information, performs operations, and then encrypts it back and saves it in the session-token
cookie. This token is then passed to the session
callback, which forms the session object that will be shared site-wide and can be accessed on the server as const session = await locals.auth()
and on the client-side as const session = $page.data.session
.
Hence, in order to update the user information, we must use the jwt
callback so that it is preserved in the session-token
cookie and later pass the information to the session
callback so that the information is accessible on both the server and the client side.
The First Step: Updating the Config Object
With this knowledge in hand, let's configure the jwt
and the session
callback in the SvelteKitAuthConfig
object.
{
callbacks: {
async jwt({ token, account, profile }) {
...
const userQuery = event.request.headers.get('query') || event.url.searchParams.get('query');
if (userQuery === 'update-user-data') {
try {
const clonedRequest = event.request.clone();
const clonedBody = clonedRequest.body;
const response = await new Response(clonedBody);
const body = await response.json();
if (!isEmpty(body)) {
token = {
...token,
...body
};
}
} catch (ex: any) {
console.log('Unable to update user data for url', ex?.message);
}
}
return token;
},
async session({ session, token }) {
if (session.user) {
if (token?.access_token) {
session.user = { ...session.user, ...token } as any;
}
}
return session;
}
}
}
Let's take a moment to understand what the above code snippet actually does:
Since the
jwt
callback is invoked for every API request intercepted by the hooks, we cherry-pick the ones that have a particular flag passed via the header or the query params, which will instruct us that we must perform data communication operations. For the sake of this example, we use the string update-user-data.Once we get hold of this flag, we will fetch the payload from the request and update the
token
property of thejwt
callback. Remember, this is the same property that will be preserved in an encrypted format in thesession-cookie
. Side Note: cookie size up to 4kb could be saved at any given time due to the browser's storage limitation. However, if we pass too much data into thetoken
property, they will be chunked into multiple cookies, i.e.,session-cookie_0
,session-cookie_1
, and so on.Finally, we pass the
token
property to thesession
callback so that it is available for use on both the client and the server side.
The Second Step: Creating the Update Endpoint
As we discussed in the last section, we need to intercept specific API requests that have flags which will enable us to kickstart the data operation. So in this section, we will create an API route that will be used by the client to send the payload which must be synced and updated on the server side.
So we create an API route in the file src/routes/api/update-user-data/+server.ts. The aim of this API route is to pass data to the jwt
callback. The API route then sends the updated data back, which we can catch by invoking the auth()
method. We then return the updated object back to the client.
import type { RequestHandler } from '@sveltejs/kit';
import { isEmpty } from 'lodash-es';
export const POST = (async ({ locals }) => {
const session = await locals.auth();
const user = session?.user;
if (!isEmpty(user) && !isEmpty(session)) {
try {
return new Response(JSON.stringify({
data: user
}), { status: 200 });
} catch (error: any) {
console.log('Error while updating user data: ', error?.message, '. Sending existing data');
return new Response(JSON.stringify({ data: user }), { status: 200 });
}
} else {
return new Response(JSON.stringify({ data: 'user is not authorized to update data' }), { status: 401 });
}
}) satisfies RequestHandler;
The Third Step: Client Initiates the Update Request
The client triggers the API route that we created in the previous section and sends the payload object along with it.
<script lang="ts">
let userData = $page.data.session?.user?.fav_num || '';
async function updateUserData() {
const request = await fetch('/api/update-user-data?query=update-user-data', {
method: 'POST',
body: JSON.stringify({
fav_num: `My favourite number is: ${Math.ceil(Math.random() * 100)}`
})
});
const response = await request.json();
userData = response?.data?.fav_num;
}
</script>
<button on:click={updateUserData} class="button">Update user data</button>
{#if userData}
<p>Updated user-data {userData}</p>
{/if}
With these three steps, the data is synced and updated by the client to the server. In the next section, we will learn how to update and sync the data from the server to the client.
Updating and Syncing Data from the Server to the Client
The only way for the server to send the information back to the client is via hydration. The server load functions, i.e., +layout.server.ts and +page.server.ts, can send data to the client by returning an object. Only objects that can be serialized using devalue can be used. Here is an example where the server load function returns the session property to the client.
NOTE: Properties returned by the layout server load function will be available to all the child routes, whereas the properties returned by the page server load will only be available to the current route. If both functions return the same property, the one that returns the last will precede over others.
// server load functions
import type { LayoutServerLoad } from './$types';
export const load: LayoutServerLoad = async (event) => {
const session = await event.locals.auth();
return { session };
};
The client can access this information using the $page.data
property.
<script lang="ts">
import { page } from '$app/stores';
let session = $page.data.session;
let user = $page.data.session.user;
</script>
Conclusion
In this article, we explored the process of exchanging data between the client and server using SvelteKitAuth. We delved into the importance of JWT and session callbacks, and how to effectively synchronize and store sessions. By configuring the jwt
and session
callbacks, creating an update endpoint, and initiating update requests from the client, we ensured seamless data synchronization from the client to the server. Additionally, we discussed how the server can send information back to the client through hydration using server load functions. By following these steps, you can maintain a consistent and secure data exchange between the client and server in your SvelteKit applications.
Here is the link to the GitHub repository with the codebase. In the next article, we will learn to how to build custom pages and handle events in SvelteKitAuth.
Subscribe to my newsletter
Read articles from Aakash Goplani directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Aakash Goplani
Aakash Goplani
Senior Frontend Dev. Mostly Angular and Svelte. I will be sharing my day-to-day learning experiences here with the community.