Configura PostgreSQL con Drizzle ORM en SvelteKit: Base de datos del blog
En este tutorial, aprenderás el proceso de configuración de una base de datos PostgreSQL en un proyecto SvelteKit utilizando Drizzle ORM.
Veremos cómo crear un entorno con Docker, definir tablas, generar un usuario administrador desde un script y finalizar con la importación de datos de prueba.
Después de esto ya podremos meternos a fondo con Svelte 5 y Sveltekit.
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 🚩
- Siguiente parte en construcción 👷
Instalar PostgreSQL
Para el tutorial vamos a utilizar Docker, pero si prefieres instalar PostgreSQL en tu máquina, puedes hacerlo desde este enlace https://www.postgresql.org/download/ siguiendo las instrucciones.
Si prefieres utilizar Docker, ve a la raíz del proyecto y crea un archivo llamado docker-compose.yml con el siguiente contenido:
services:
blog-db:
container_name: cosasdedevs_pg
image: postgres:16.2
restart: always
volumes:
- ./postgres:/var/lib/postgresql/data
ports:
- 5432:5432
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=123456
- POSTGRES_DB=blogdb
En este archivo docker-compose.yml
, configuramos un servicio llamado blog-db
que utiliza la imagen oficial de PostgreSQL en su versión 16.2.
Hemos definido un volumen local (./postgres
) para almacenar los datos de la base de datos, asegurando que no se pierdan incluso si el contenedor es eliminado.
El servicio estará disponible en el puerto 5432
, que vinculamos tanto en el host como en el contenedor. Además, configuramos las variables de entorno necesarias para la autenticación y especificamos el nombre de la base de datos (blogdb
).
Ahora lanza el siguiente comando y con eso levantaremos el contenedor y la base de datos:
docker-compose up
El siguiente paso es añadir en el archivo .gitignore
la carpeta postgres
para evitar que se suba al repositorio:
postgres
Instalación y configuración de Drizzle ORM
Para trabajar con nuestra base de datos, vamos a utilizar Drizzle ORM
, para instalarlo, vamos a la terminal y lanzamos los siguientes comandos en la raíz del proyecto:
npm i drizzle-orm pg dotenv
npm i -D drizzle-kit @types/pg tsx
Una vez hecho esto, crearemos el archivo .env
en la raíz del proyecto para guardar nuestras variables de entorno y ahí añadiremos la siguiente:
DATABASE_URL="postgresql://postgres:123456@localhost:5432/blogdb?schema=public"
Recuerda sustituir el usuario, contraseña, puerto y nombre de la base de datos si no estás utilizando los mismos que utilicé yo en el archivo docker-compose.yml
.
Si vas a subir el proyecto a un repositorio, te recomiendo que crees un archivo llamado .env.example
con ejemplos de las variables de entorno que vayamos añadiendo para que las tengas de ejemplo en un futuro por si las necesitas.
Creación de la conexión a la base de datos y el schema
El siguiente paso es establecer la conexión a la base de datos que utilizaremos en nuestro proyecto. Para ello, primero debemos crear una carpeta llamada server
dentro de la carpeta lib
. Dentro de server
, crearemos otra carpeta llamada db
y, en su interior, un archivo llamado index.ts
con el siguiente contenido:
import { DATABASE_URL } from "$env/static/private";
import { drizzle } from "drizzle-orm/node-postgres";
export default drizzle(DATABASE_URL);
En este archivo, primero importamos la variable de entorno DATABASE_URL
utilizando el sistema de gestión de variables de SvelteKit.
Es importante tener en cuenta que las variables sin el prefijo PUBLIC_
solo están disponibles en los archivos que se ejecutan exclusivamente del lado del servidor, garantizando así su seguridad.
A continuación, importamos drizzle
para PostgreSQL y, finalmente, configuramos la conexión a la base de datos utilizando dicha variable de entorno.
Ahora vamos a crear el schema
. Para que te hagas una idea, este es diagrama de tablas que vamos a utilizar:
Como puedes ver, tenemos una tabla para usuarios, otra para controlar el sistema de sesiones, la tabla donde guardaremos los posts y la tabla donde almacenaremos los comentarios de los usuarios.
Para convertir este diagrama a código que después podamos utilizar para generar nuestras tablas, vamos al proyecto y creamos un archivo con el nombre src\lib\server\db\schema.ts
con el siguiente contenido:
import type { InferSelectModel } from "drizzle-orm";
import { boolean, integer, pgTable, text, timestamp, varchar } from "drizzle-orm/pg-core";
export const usersTable = pgTable("users", {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
username: varchar({ length: 255 }).notNull(),
password: varchar({ length: 255 }).notNull(),
email: varchar({ length: 255 }).notNull().unique(),
createdAt: timestamp("created_at", {
withTimezone: true,
mode: "date"
}).defaultNow().notNull(),
isActive: boolean("is_active").default(true).notNull(),
isAdmin: boolean("is_admin").default(false).notNull(),
});
export const sessionsTable = pgTable("sessions", {
id: text("id").primaryKey(),
userId: integer("user_id")
.notNull()
.references(() => usersTable.id),
expiresAt: timestamp("expires_at", {
withTimezone: true,
mode: "date"
}).notNull()
});
export const postsTable = pgTable("posts", {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
userId: integer("user_id")
.notNull()
.references(() => usersTable.id),
title: varchar({ length: 255 }).notNull().unique(),
slugUrl: varchar('slug_url', { length: 255 }).notNull().unique(),
imageHeader: varchar('image_header', { length: 255 }).notNull(),
content: text().notNull(),
createdAt: timestamp("created_at", {
withTimezone: true,
mode: "date"
}).defaultNow().notNull(),
isDraft: boolean("is_draft").default(true).notNull(),
});
export const commentsTable = pgTable("comments", {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
userId: integer("user_id")
.notNull()
.references(() => usersTable.id),
postId: integer("post_id")
.notNull()
.references(() => postsTable.id),
content: text().notNull(),
createdAt: timestamp("created_at", {
withTimezone: true,
mode: "date"
}).defaultNow().notNull(),
});
export type UserEntity = InferSelectModel<typeof usersTable>;
export type SessionEntity = InferSelectModel<typeof sessionsTable>;
export type PostEntity = InferSelectModel<typeof postsTable>;
export type CommentEntity = InferSelectModel<typeof commentsTable>;
Este código define el esquema de tablas para nuestra base de datos PostgreSQL usando Drizzle ORM, además de generar tipos TypeScript para representar los datos. A continuación, te explico cada sección en detalle:
1. Tabla usersTable
Esta tabla representa los usuarios del sistema.
id
: Entero auto-generado, clave primaria.username
: Cadena de caracteres obligatoria, máximo 255 caracteres.password
: Cadena de caracteres obligatoria que almacenará contraseñas encriptadas.email
: Cadena obligatoria y única para identificar usuarios.createdAt
: Fecha y hora de creación con zona horaria, valor por defecto, la fecha/hora actual.isActive
: Booleano que indica si el usuario está activo, por defectotrue
.isAdmin
: Booleano que indica si el usuario tiene privilegios de administrador, por defectofalse
.
2. Tabla sessionsTable
Esta tabla almacena las sesiones activas de los usuarios.
id
: Cadena de texto única y clave primaria para identificar sesiones.userId
: Entero obligatorio que referencia al ID de un usuario enusersTable
.expiresAt
: Fecha y hora obligatoria que indica cuándo expira la sesión.
3. Tabla postsTable
Representa los posts o publicaciones creadas por los usuarios.
id
: Entero auto-generado, clave primaria.userId
: Entero obligatorio que referencia al ID del usuario que creó el post.title
: Cadena obligatoria, única, que almacena el título del post.slugUrl
: Cadena obligatoria, única, que sirve como URL amigable del post.imageHeader
: Cadena obligatoria que almacena la URL de la imagen principal del post.content
: Texto obligatorio que contiene el contenido del post.createdAt
: Fecha y hora de creación con valor por defecto la fecha/hora actual.isDraft
: Booleano que indica si el post es un borrador, por defectotrue
.
4. Tabla commentsTable
Almacena los comentarios hechos en los posts.
id
: Entero auto-generado, clave primaria.userId
: Entero obligatorio que referencia al ID del usuario que hizo el comentario.postId
: Entero obligatorio que referencia al ID del post enpostsTable
.content
: Texto obligatorio que almacena el contenido del comentario.createdAt
: Fecha y hora de creación con valor por defecto la fecha/hora actual.
5. Tipos TypeScript
Los tipos definidos permiten usar las tablas como modelos fuertemente tipados en TypeScript:
UserEntity
: Representa los datos de la tablausersTable
.SessionEntity
: Representa los datos de la tablasessionsTable
.PostEntity
: Representa los datos de la tablapostsTable
.CommentEntity
: Representa los datos de la tablacommentsTable
.
Una vez tenemos definido el schema, debemos crear el archivo de configuración de drizzle para poder crear las tablas en la base de datos. Para ello, debemos crear un archivo llamado drizzle.config.ts en la raiz del proyecto con el siguiente contenido:
import 'dotenv/config';
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
out: './drizzle',
schema: './src/lib/server/db/schema.ts',
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL!,
},
});
En este archivo configuramos lo siguiente:
- Ubicación del esquema: Especificamos la ruta al archivo que contiene el esquema de la base de datos que definimos anteriormente.
- Carpeta de archivos generados: Indicamos que los archivos generados por Drizzle, como las migraciones, se guardarán en la carpeta
./drizzle
. - Tipo de base de datos: Declaramos que estamos utilizando PostgreSQL como sistema de gestión de bases de datos.
- Conexión a la base de datos: Configuramos la conexión utilizando la URL definida en las variables de entorno.
Es importante notar que, en este caso, no utilizamos el sistema de variables de entorno propio de SvelteKit. En su lugar, empleamos dotenv
y el soporte nativo de Node.js.
Esto se debe a que este archivo está ubicado en la raíz del proyecto, donde las variables de entorno de SvelteKit no están disponibles. Por lo tanto, recurrimos a dotenv
para cargar dichas variables y utilizarlas en esta configuración.
Crear las tablas en nuestra base de datos
Con esta configuración y con el contenedor de la base de datos ya podemos crear nuestras tablas. Para ello debemos usar el siguiente comando:
npx drizzle-kit push
Script para crear nuestro usuario de administración
El siguiente paso a realizar será un script con el que vamos a crear nuestro usuario de administración para poder hacer pruebas y así ver como funciona Drizzle ORM.
Para ello primero vamos a instalar la librería @node-rs/argon2 que utilizaremos para encriptar nuestras contraseñas. Recuerda y esto es muy importante, que por la ley de protección de datos, al menos en la UE, no puedes almacenar las contraseñas en texto plano.
El comando para instalarla es el siguiente:
npm i @node-rs/argon2
Una vez hecho esto, crearemos el archivo src\lib\server\db\create-user-admin.ts
y añadiremos el siguiente código:
import 'dotenv/config';
import { usersTable } from "$lib/server/db/schema";
import { hash, verify } from "@node-rs/argon2";
import { eq } from "drizzle-orm";
import { drizzle } from "drizzle-orm/node-postgres";
const db = drizzle(process.env.DATABASE_URL || '');
export async function hashPassword(password: string): Promise<string> {
return await hash(password);
}
export async function verifyPasswordHash(hash: string, password: string): Promise<boolean> {
return await verify(hash, password);
}
async function main() {
const passwordRaw = 'admin123';
const user: typeof usersTable.$inferInsert = {
username: 'Alberto',
password: await hashPassword(passwordRaw),
email: 'alberto@cosasdedevs.com',
isAdmin: true,
};
await db.insert(usersTable).values(user);
console.log('New user created!');
const getUsers = await db
.select()
.from(usersTable)
.where(eq(usersTable.email, user.email));
if (!getUsers[0]) {
throw new Error('User not exists');
}
const isValidPassword = await verifyPasswordHash(getUsers[0].password, passwordRaw);
if (!isValidPassword) {
throw new Error('Invalid password');
}
console.log('User has been created successfully');
}
main();
Este script se encarga de crear el primer usuario de la base de datos y será el usuario que utilizaremos como usuario administrador. A continuación te explico el código en profundidad:
- Configuración de la base de datos
- Se importa y configura el módulo
dotenv
para cargar las variables de entorno necesarias. - Se establece una conexión a la base de datos PostgreSQL utilizando Drizzle ORM.
- Se importa y configura el módulo
- Funciones de manejo de contraseñas
hashPassword
: Esta función toma una contraseña en texto plano y devuelve su versión hasheada utilizando la librería@node-rs/argon2
.verifyPasswordHash
: Esta función compara una contraseña en texto plano con su hash para verificar si coinciden.
- Función principal (
main
)- Creación de un usuario: Se define un objeto con los datos del usuario, incluyendo un hash de su contraseña, y se inserta en la tabla
usersTable
mediante el ORM Drizzle. - Validación del usuario:
- Se consulta la base de datos para verificar que el usuario fue creado correctamente.
- Se valida que la contraseña proporcionada coincide con el hash almacenado en la base de datos.
- Mensajes de confirmación: Si todo es correcto, se imprimen mensajes en la consola indicando que el usuario fue creado y que la validación de la contraseña fue exitosa.
- Creación de un usuario: Se define un objeto con los datos del usuario, incluyendo un hash de su contraseña, y se inserta en la tabla
Puedes sustituir los datos del usuario en el objeto user
(como username
, email
y passwordRaw
) por los que prefieras para adaptarlo a tus necesidades.
Importar datos de posts a la base de datos
Para tener unos datos de prueba, he creado dentro del proyecto un archivo con unos inserts para los posts, los puedes encontrar aquí:
https://github.com/albertorc87/blog-svelte-5/blob/tutorial-3-bases-de-datos/data/test-data.sql
Puedes utilizar una herramienta para conectarte a la base de datos e insertar los datos. Yo he utilizado TablePlus. Te dejo en enlace para que puedas descargarla:
https://tableplus.com/download
Importar imágenes de los posts de prueba
También he subido al repositorio las imágenes que utilizaremos para los posts, se encuentran aquí:
https://github.com/albertorc87/blog-svelte-5/tree/tutorial-3-bases-de-datos/data/test-images
Necesito que las guardes dentro de la carpeta assets/posts (la carpeta posts no existe, debes crearla).
Actualizar .gitignore para que no se suban las imágenes al repo
Por último, si vas a subir el proyecto a un repositorio, añade dentro del archivo .gitignore la siguiente línea para que no se suban estas imágenes:
static/posts
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-3-bases-de-datos
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 👋.