Página de registro de nuestro blog con Svelte 5 y SvelteKit
En este tutorial, veremos cómo crear una página de registro utilizando Svelte 5 y SvelteKit. Crearemos el formulario y lo enviaremos a nuestro servidor donde validaremos la información recibida y crearemos el usuario.
Antes de empezar 🛑 y como siempre, si te has perdido alguno de los tutoriales de esta serie, aquí te dejo todos los tutoriales escritos hasta ahora:
- Crea una Web Fullstack con Svelte 5 y SvelteKit
- Enrutamiento y Cabecera Responsive en SvelteKit: Configuración Inicial para Tu Blog
- Configura PostgreSQL con Drizzle ORM en SvelteKit: Base de datos del blog
- Parte 1: Cargar posts en la página de inicio y detalle del post con Svelte 5 y Sveltekit
- Parte 2: Cargar posts en la página de inicio y detalle del post con Svelte 5 y Sveltekit
- Autenticación en Svelte 5 y SvelteKit: Crea un Login y Gestión de Sesión con Lucia-Auth
- Página de registro de nuestro blog con Svelte 5 y SvelteKit 🚩
- Siguiente parte en construcción 👷
Mover los estilos de los formularios en login a app.css
Dado que los estilos utilizados en el registro serán los mismos que en el login, vamos a centralizarlos para facilitar el mantenimiento. Para ello, moveremos los estilos de src/routes/login/+page.svelte
al archivo global src/app.css
. Además, crearemos una clase específica llamada form-auth
para sustituir a la clase form
. Esto nos permitirá evitar colisiones en caso de que necesitemos crear otros formularios con formatos diferentes.
Abrimos el archivo src/app.css
y añadimos el siguiente contenido:
.form-auth {
max-width: 400px;
margin: 0 auto;
padding: 1rem;
}
.form-auth div {
display: flex;
flex-direction: column;
}
.form-auth label {
text-align: left;
width: 100%;
margin-bottom: 0.5rem;
}
.form-auth input {
font-size: 1.2rem;
width: 100%;
padding: 0.5rem;
border-radius: 5px;
border: 1px solid #999999;
background-color: #f0f0f0;
margin-bottom: 1rem;
}
Nuevas utilidades para nuestro proyecto
Vamos a necesitar un par de utilidades nuevas en nuestro proyecto. Una ya existe que es la de convertir un dato de tipo FormDataEntryValue
a string
y otro para validar si un email tiene formato correcto. Para el primer caso vamos a crear un archivo llamado src\lib\utils\form-utils.ts
con el siguiente contenido:
export function convertToString(value: FormDataEntryValue | null): string {
return value ? value.toString() : '';
}
Esta función ya la teníamos en src\routes\login\+page.server.ts
. Podéis eliminarla e importarla desde el nuevo archivo.
El siguiente archivo de utilidades se llamará src/lib/utils/validations.ts
y en él guardaremos una función para validar si el email tiene el formato correcto. Para una validación más exhaustiva podríamos valernos de una librería como https://github.com/validatorjs/validator.js, pero para nuestro tutorial podemos valernos con el código siguiente:
export function isValidEmail(email: string | null): boolean {
if (!email) {
return false;
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
Funciones para crear el usuario y validar si el email ya está en uso
Para el siguiente paso, vamos a añadir dos nuevas funciones en el archivo src\lib\server\db\db-users.ts
. Estas se encargarán de crear un usuario y comprobar si ya está en uso un email para que no se puedan crear dos cuentas con un mismo email:
import { eq } from 'drizzle-orm';
import { db } from './db';
import { usersTable, type UserEntity } from './schema';
import { hashPassword, verifyPasswordHash } from '$lib/utils/password-utils';
export async function getUserByEmailAndPassword(
email: string,
rawPassword: string
): Promise<UserEntity> {
const getUsers = await db.select().from(usersTable).where(eq(usersTable.email, email));
if (!getUsers[0]) {
throw new Error('User not exists');
}
if (!verifyPasswordHash(getUsers[0].password, rawPassword)) {
throw new Error('Invalid password');
}
return getUsers[0];
}
export async function createUser(
username: string,
passwordRaw: string,
email: string
): Promise<UserEntity> {
const user: typeof usersTable.$inferInsert = {
username,
password: await hashPassword(passwordRaw),
email: email
};
await db.insert(usersTable).values(user);
const getUsers = await db.select().from(usersTable).where(eq(usersTable.email, user.email));
return getUsers[0];
}
export async function existsEmailUser(email: string): Promise<boolean> {
const getUsers = await db.select().from(usersTable).where(eq(usersTable.email, email));
if (!getUsers[0]) {
return false;
}
return true;
}
Como puedes observar, a la hora de crear el usuario utilizamos la función hashPassword
que creamos anteriormente para encriptar la contraseña.
Página de registro
Una vez hecho esto vamos a crear la página de registro. Para ello vamos al archivo src\routes\registro\+page.svelte
y sustituimos el código que añadimos al principio por el siguiente:
<script lang="ts">
import { Button } from '$components';
import MessageError from '$components/MessageError.svelte';
import type { ActionData } from './$types';
let { form }: { form: ActionData } = $props();
let username: string = $state(form?.values?.username || '');
let email: string = $state(form?.values?.email || '');
let password: string = $state(form?.values?.password || '');
let repeatPassword: string = $state('');
let messageError: string | undefined = $state(form?.error);
let isButtonDisabled: boolean = $derived(
email.trim().length === 0 ||
password.trim().length === 0 ||
username.trim().length === 0 ||
repeatPassword.trim().length === 0
);
function send(event: MouseEvent) {
if (email.trim().length === 0 || password.trim().length === 0) {
event.preventDefault();
return;
}
}
function clearError() {
messageError = undefined;
}
</script>
<h1>Registro</h1>
<form method="POST" class="form-auth">
<div>
<label for="email">Nombre de usuario</label>
<input name="username" type="text" bind:value={username} onfocus={clearError} />
</div>
<div>
<label for="email">Email</label>
<input name="email" type="email" bind:value={email} onfocus={clearError} />
</div>
<div>
<label for="password">Contraseña</label>
<input name="password" type="password" bind:value={password} onfocus={clearError} />
</div>
<div>
<label for="repeatPassword">Repetir contraseña</label>
<input name="repeatPassword" type="password" bind:value={repeatPassword} onfocus={clearError} />
</div>
<Button onclick={send} isDisabled={isButtonDisabled}>Login</Button>
{#if messageError}
<MessageError message={messageError} />
{/if}
</form>
El código es muy similar al del formulario de login, con algunas diferencias. Aquí también gestionamos los estados de username
y repeatPassword
, además de los ya conocidos email
, password
y messageError
.
Para determinar si el botón de envío debe estar deshabilitado, utilizamos $derived
, una funcionalidad de Svelte
que permite calcular valores derivados de otros estados reactivos. Esto asegura que el valor de isButtonDisabled
se actualice automáticamente cada vez que cambie el estado de cualquiera de las variables que componen la condición.
Por último, crearemos el archivo src\routes\registro\+page.server.ts
con el siguiente contenido:
import { createUser, existsEmailUser, getUserByEmailAndPassword } from '$lib/server/db/db-users';
import { fail, redirect } from '@sveltejs/kit';
import type { PageServerLoad, Actions } from './$types';
import {
createSession,
generateSessionToken,
sessionCookieName,
setSessionTokenCookie,
validateSessionToken
} from '$lib/server/db/auth';
import { convertToString } from '$lib/utils/form-utils';
import { isValidEmail } from '$lib/utils/validations';
export const load: PageServerLoad = async ({ cookies }) => {
const infoSession = await validateSessionToken(cookies.get(sessionCookieName));
if (infoSession.session !== null) {
redirect(302, '/');
}
};
export const actions = {
default: async (event) => {
const data = await event.request.formData();
const values = {
email: convertToString(data.get('email')),
username: convertToString(data.get('username')),
password: convertToString(data.get('password'))
};
if (
!data.get('email') ||
!data.get('username') ||
!data.get('password') ||
!data.get('repeatPassword')
) {
return fail(400, {
error: 'Debes enviar el nombre de usuario, email y la contraseña',
values
});
}
if (data.get('password') !== data.get('repeatPassword')) {
return fail(400, { error: 'Las contraseñas no coinciden', values });
}
if (!isValidEmail(convertToString(data.get('email')))) {
return fail(400, { error: 'El email enviado no es válido', values });
}
const username = convertToString(data.get('username')).trim();
const email = convertToString(data.get('email')).trim();
const password = convertToString(data.get('password')).trim();
if (username.length < 3 || username.length > 50) {
return fail(400, {
error: 'El nombre de usuario debe tener entre 3 y 50 caracteres',
values
});
}
if (password.length < 3 || password.length > 20) {
return fail(400, {
error: 'El nombre de usuario debe tener entre 3 y 20 caracteres',
values
});
}
try {
const existsEmail = await existsEmailUser(email);
if (existsEmail) {
return fail(400, {
error: 'El email enviado ya está en uso',
values
});
}
const user = await createUser(username, password, email);
const token = await generateSessionToken();
const dataSession = await createSession(token, user.id);
setSessionTokenCookie(event, token, dataSession.expiresAt);
return { success: true };
} catch (err) {
return fail(400, {
error: 'Ha ocurrido un error en la creación del usuario, por favor intentalo más tarde',
values
});
}
}
} satisfies Actions;
Este archivo es similar a la parte del servidor que manejamos anteriormente para el login, pero aquí nos enfocamos en validar los datos enviados desde el formulario y gestionar el flujo completo de registro del usuario.
Primero, verificamos que todos los campos requeridos estén presentes y validamos que la contraseña coincida con su repetición. También comprobamos que el tamaño del username
y password
esté dentro de los límites establecidos. Usamos la función creada previamente para validar que el email tenga un formato correcto y que no esté ya en uso en la base de datos.
Si todas las validaciones son correctas, creamos el usuario con los datos proporcionados. Después de registrar al usuario, generamos un token de sesión y configuramos una cookie para mantener la sesión activa, asegurándonos de que el usuario sea autenticado automáticamente tras registrarse.
En caso de error, devolvemos los valores ingresados junto con un mensaje descriptivo del problema, lo que facilita al usuario corregir únicamente los campos necesarios sin necesidad de rellenar el formulario nuevamente.
Y eso es todo por este tutorial. Recordad que cualquier duda podéis escribirla en la caja de comentarios y también os dejo el enlace a la rama que corresponde a este tutorial por si tenéis cualquier problema:
https://github.com/albertorc87/blog-svelte-5/tree/tutorial-6-registro
Espero que este post te ayude y como siempre, te recomiendo seguirme en Twitter para estar al tanto de los nuevo contenido. Ahora también puedes seguirme en Instagram donde estoy subiendo tips, tutoriales en vídeo e información sobre herramientas para developers.
Por último os dejo mi guía para aprender a trabajar con APIs donde explico todo el funcionamiento de una API, el protocolo HTTP y veremos como construir una API con arquitectura REST.
Nos leemos 👋.