loading...
Cover image for Prisma, un toolkit para bases de datos (¿ORM?) para TypeScript y Node.js

Prisma, un toolkit para bases de datos (¿ORM?) para TypeScript y Node.js

denispixi profile image Denis Adhemar ・9 min read

Prisma es un kit de herramientas (toolkit) de base de datos de código abierto. Reemplaza los ORM tradicionales y facilita el acceso a la base de datos con un generador de consultas o query builder automáticamente generado y type-safe que se adapta a nuestro esquema de base de datos.

Se compone principalmente de las siguientes partes:

  • Prisma Client: generador de consultas, autogenerado y seguro para Node.js y TypeScript
  • Prisma Migrate (experimental): sistema de migración y modelado de datos declarativos
  • Prisma Studio (experimental): GUI para ver y editar datos en su base de datos

En este artículo veremos - mediante un ejemplo sencillo - cómo usar Prisma Client, la herramienta que nos permite interactuar con la base de datos desde nuestra aplicación de manera fácil y sencilla. Prisma Client proporciona una API intuitiva (auto-completado, validaciones de tipos, etc) y flexible para casos de uso comunes como paginación o filtrado y nos ahorra los boilerplates CRUD repetitivos.

1) Configurar Prisma

En éste ejemplo haremos un API REST sencilla con Express.js y TypeScript para una biblioteca con las operaciones CRUD para Libros, Autores y Reviews de los libros.

Prisma actualmente soporta PostgreSQL, MySQL y SQLite. Esta en desarrollo el soporte para otras BDs tanto SQL como NoSQL. Para éste ejemplo por razones de simplicidad estaremos usando SQLite. Puedes descargar el archivo para este ejemplo aquí.

Lo primero que haremos será crear un folder para nuestro proyecto, movernos dentro de éste e inicializar un proyecto de JavaScript:

$ mkdir prisma-example && cd prisma-example && npm init -y

Ahora instalaremos algunas dependencias:

$ npm i express morgan
$ npm i @prisma/cli typescript concurrently nodemon @types/express @types/morgan -D

Estamos instalando las dependencias de Express, TypeScript, sus definiciones de tipos correspondientes, y lo que nos interesa aquí es @prisma/cli: es la herramienta que nos permitirá autogenerar nuestro esquema de Prisma a partir de nuestro esquema de base de datos.

Creamos el archivo tsconfig.json para las opciones de TypeScript:

{
  "compilerOptions": {
    "sourceMap": true,
    "outDir": "dist",
    "strict": true,
    "lib": ["esnext"],
    "esModuleInterop": true
  }
}

Y reemplazamos la parte de "scripts" del archivo package.json con lo siguiente:

{
    //...
    "scripts": {
        "start": "concurrently \"tsc -w\" \"nodemon dist/index.js\""
    }
    //...
}

Hecho esto, podemos invocar a la CLI de Prisma mediante npx. Inicializamos Prisma con el sgte. comando:

$ npx prisma init

Éste comando creará una nueva carpeta llamada prisma con los siguientes archivos:

  • schema.prisma: El esquema de Prisma con la conexión a base de datos y el generador de Prisma Client
  • .env: Archivo dotenv para definir variables de entorno (utilizado para la conexión a la base de datos)

Dentro de la carpeta prisma, colocamos el archivo prisma_example.db que descargamos anteriormente.

2) Conexión a la base de datos

Editamos el archivo schema.prisma y reemplazamos el campo datasource con lo siguiente:

datasource sqlite {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}

Y reemplazamos el contenido del archivo .env con lo siguiente:

DATABASE_URL="file:./prisma_example.db"

La base de datos SQLite que hemos descargado tiene las sgtes 3 tablas ya creadas:

create table author(
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name VARCHAR(50),
    surname VARCHAR(50),
    age INTEGER
);

create table book(
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    title VARCHAR(50),
    page_count INTEGER,
    published_date DATE,
    genre VARCHAR(50),
    author_id INTEGER,
    CONSTRAINT fk_author
        FOREIGN KEY (author_id)
        REFERENCES author(id)
);

create table review(
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    nickname VARCHAR(50),
    content TEXT,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    book_id INTEGER,
    CONSTRAINT fk_book
        FOREIGN KEY (book_id)
        REFERENCES book(id)
);

Como podemos ver, la tabla book tiene una llave foránea a la tabla author, y la tabla review tiene una llave foránea a la tabla book. Para obtener nuestro modelo de datos, usaremos la herramienta de análisis de Prisma, la cual lee nuestro esquema de base de datos y lo transforma en un modelo de datos de Prisma. Para ello, ejecutamos el siguiente comando:

$ npx prisma introspect

El comando anterior sobre escribe el archivo schema.prisma, insertando el modelo de datos de Prisma obtenido durante el análisis de la base de datos:

generator client {
  provider = "prisma-client-js"
}

datasource sqlite {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}
// LAS SIGUIENTES LINEAS FUERON AGREGADAS
model author {
  age     Int?
  id      Int     @default(autoincrement()) @id
  name    String?
  surname String?
  book    book[]
}

model book {
  author_id      Int?
  genre          String?
  id             Int       @default(autoincrement()) @id
  page_count     Int?
  published_date DateTime?
  title          String?
  author         author?   @relation(fields: [author_id], references: [id])
  review         review[]
}

model review {
  book_id    Int?
  content    String?
  created_at DateTime? @default(now())
  id         Int       @default(autoincrement()) @id
  nickname   String?
  book       book?     @relation(fields: [book_id], references: [id])
}

Éste modelo de datos es una representacion declarativa de nuestro esquema de base de datos, y será tomado por Prisma Client, el cual expondrá queries en TS/JS que estan adaptadas a éste modelo.

Adicionalmente, cambiaremos algunos nombres que están en singular y reprensentan conjuntos de datos para que tengan más sentido y sean mas entendibles al usar el cliente de Prisma en nuestra aplicación:

generator client {
  provider = "prisma-client-js"
}

datasource sqlite {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}

model author {
  age     Int?
  id      Int     @default(autoincrement()) @id
  name    String?
  surname String?
  // LA SIGUIENTE LINEA FUE CAMBIADA
  books   book[]
}

model book {
  author_id      Int?
  genre          String?
  id             Int       @default(autoincrement()) @id
  page_count     Int?
  published_date DateTime?
  title          String?
  author         author?   @relation(fields: [author_id], references: [id])
  // LA SIGUIENTE LINEA FUE CAMBIADA
  reviews        review[]
}

model review {
  book_id    Int?
  content    String?
  created_at DateTime? @default(now())
  id         Int       @default(autoincrement()) @id
  nickname   String?
  book       book?     @relation(fields: [book_id], references: [id])
}

3) Generar Prisma Client para nuestro modelo de datos

Ahora que tenemos nuestro modelo de datos de Prisma, vamos a generar nuestro Prisma Client para poder usarlo en nuestra aplicación de Express. Para ello debemos instalar el modulo @prisma/client

$ npm i @prisma/client

y luego generamos nuestro cliente con el comando:

$ npx prisma generate

Genial!, ya tenemos nuestro Prisma Client listo para ser usado en nuesta aplicación. Recuerda que cada vez que agreguemos o modifiquemos alguna tabla en nuestra base de datos, debemos ejecutar nuevamente los siguientes comandos para actualizar nuestro modelo de datos de prisma asi como nuestro Prisma Client:

$ npx prisma instrospect
$ npx prisma generate

Ahora, pasemos a crear nuestra API REST con Express.

4) Crear el API REST con Express

Primero creamos una carpeta src en la raiz de nuestro proyecto y dentro creamos un archivo index.ts con el siguiente contenido:

// src/index.ts
import express, { Router } from 'express'
import morgan from 'morgan'
import {
  getBooks, getBookById, createBook, deleteBookById, updateBookById,
  getAuthors, getAuthorById, createAuthor, updateAuthor, deleteAuthorById,
  createReview,
} from './controllers'

const app = express()

// middlewares
app.use(morgan('tiny'))
app.use(express.json())
app.use(express.urlencoded({ extended: false }))

const router = Router()

// Rutas para los libros
router.get('/books', getBooks)
router.get('/books/:id', getBookById)
router.post('/books', createBook)
router.patch('/books', updateBookById)
router.delete('/books/:id', deleteBookById)
// Rutas para los autores
router.get('/authors', getAuthors)
router.get('/authors/:id', getAuthorById)
router.post('/authors', createAuthor)
router.patch('/authors', updateAuthor)
router.delete('/authors/:id', deleteAuthorById)
// Rutas para reviews
router.post('/reviews', createReview)
// pasamos las rutas a nuestro servidor
app.use(router)
// levantamos nuestro servidor en el puerto 5000
app.listen(5000, () => console.log('Server on port 5000'))

Aquí simplemente estamos creando nuestro servidor y algunas rutas para nuestra API. Tambien creamos un archivo controllers.ts con el siguiente contenido:

// src/controllers.ts
import { Request as Req, Response as Res } from 'express'

export const getBooks = (req: Req, res: Res) => {
  res.send('Hola mundo!!')
}
export const getBookById = (req: Req, res: Res) => { }
export const createBook = (req: Req, res: Res) => { }
export const updateBookById = (req: Req, res: Res) => { }
export const deleteBookById = (req: Req, res: Res) => { }
export const getAuthors = (req: Req, res: Res) => { }
export const getAuthorById = (req: Req, res: Res) => { }
export const createAuthor = (req: Req, res: Res) => { }
export const updateAuthor = (req: Req, res: Res) => { }
export const deleteAuthorById = (req: Req, res: Res) => { }
export const createReview = (req: Req, res: Res) => { }

Una vez hecho ésto, ejecutamos el siguiente comando para levantar nuestro servidor local:

$ npm start

Ahora si llamamos a la ruta http://localhost:5000/books/ desde Postman o Insomina o Curl o algun otro cliente REST, veremos que nos responde el texto Hola Mundo!

Alt Text

5) Usando Prisma Client

Por fin llego el momento de ver a Prisma Client en acción! Comenzamos por importar e instanciar el módulo:

// src/controllers.js
import { PrismaClient } from "@prisma/client"
const prisma = new PrismaClient()
//...

Ahora en nuestra función getBooks, hacemos una query al modelo book y devolvemos el resultado :

// src/controllers.js
// ...
// Colocamos `async` porque utilizaremos código asíncrono
export const getBooks = async (req: Req, res: Res) => {
  const books = await prisma.book.findMany()
  console.table(books)
  res.json({ books })
}
// ...

Aquí podemos ver cómo el autocompletado nos indica los modelos y los métodos que tenemos disponibles:

Alt Text
Alt Text

Volvemos a invocar la ruta http://localhost:5000/books/ y vemos el resultado de nuestra consulta, tanto en la consola como en nuestro cliente REST:

Alt Text
Alt Text

Actualmente tenemos unos cuantos libros en nuestra base de datos, pero no sabemos quien los escribió ☹️. Veamos los datos de los autores! para ello modificamos nuestra función de la siguente manera:

// src/controllers.js
// ...
export const getBooks = async (req: Req, res: Res) => {
  const books = await prisma.book.findMany({
    include: {
      author: true
    }
  })
  res.json({ books })
}
// ...

Una vez mas, vemos como el autocompletado nos ayuda muchísimo a saber lo que podemos solicitar 😎:

Alt Text

Invocamos a nuestra API y vemos ahora sí la información de los autores 🤓:

Alt Text

Podemos buscar también un libro específico por ID, para ello vamos a modificar nuestra función getBookById de la siguiente manera:

// src/controllers.ts
// ...
export const getBookById = async (req: Req, res: Res) => {
  // éste parametro viene de la ruta `/books/:id`
  const bookId = Number(req.params.id);
  const book = await prisma.book.findOne({
    where: { id: bookId },
    include: { author: true }
  })
  if (book)
    res.json({ book })
  else
    res.status(404).json({ message: 'Book not found!' })
}
// ...

Invocamos la ruta http://localhost:5000/books/2:

Alt Text

Y que pasaría si invocamos un libro que no existe?

Alt Text

Ahora creemos un libro 🤓. Modificamos nuestra función createBook:

// src/controllers.ts
// ...
export const createBook = async (req: Req, res: Res) => {
  const { title, author_id, genre, page_count, published_date } = req.body
  const createdBook = await prisma.book.create({
    data: {
      title,
      genre,
      page_count,
      published_date,
      author: {
        connect: {
          id: author_id
        }
      }
    },
    include: { author: true }
  })
  res.status(201).json({ createdBook })
}
// ...

Aqui estamos extrayendo los valores del request y le estamos diciendo a prisma que cree un nuevo libro con ésta información, adicionalmente estamos indicando que el autor va a hacer referencia a un elemento del modelo autor con dicho ID.

Finalmente implementaremos los metodos para actualizar y eliminar libros, y haremos lo propio para los modelos author y review. Nuestro archivo controllers.ts debe quedar asi:

import { Request as Req, Response as Res } from 'express'
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()

export const getBooks = async (req: Req, res: Res) => {
  const books = await prisma.book.findMany({
    include: {
      author: true,
      reviews: {
        select: {
          nickname: true,
          content: true,
          created_at: true
        }
      }
    }
  })
  res.json({ books })
}

export const getBookById = async (req: Req, res: Res) => {
  const bookId = Number(req.params.id);
  const book = await prisma.book.findOne({
    where: { id: bookId },
    include: { author: true }
  })
  if (book)
    res.json({ book })
  else
    res.status(404).json({ message: 'Book not found!' })
}

export const createBook = async (req: Req, res: Res) => {
  const { title, author_id, genre, page_count, published_date } = req.body
  const createdBook = await prisma.book.create({
    data: {
      title,
      genre,
      page_count,
      published_date,
      author: {
        connect: {
          id: author_id
        }
      }
    },
    include: { author: true }
  })
  res.json({ createdBook })
}

export const updateBookById = async (req: Req, res: Res) => {
  const bookId = Number(req.params.id)
  const updatedBook = await prisma.book.update({
    where: { id: bookId },
    data: req.body,
    include: { author: true }
  })
  res.json({ updatedBook })
}

export const deleteBookById = async (req: Req, res: Res) => {
  const bookId = Number(req.params.id)
  const deletedBook = await prisma.book.delete({
    where: { id: bookId },
    include: { author: true }
  })
  res.json({ deletedBook })
}

export const getAuthors = async (req: Req, res: Res) => {
  const authors = await prisma.author.findMany({
    include: {
      books: {
        select: {
          title: true,
          genre: true
        }
      }
    }
  })
  res.json({ authors })
}

export const getAuthorById = async (req: Req, res: Res) => {
  const authorId = Number(req.params.id);
  const author = await prisma.author.findOne({
    where: { id: authorId },
    include: {
      books: {
        select: {
          title: true,
          genre: true
        }
      }
    }
  })
  if (author)
    res.json({ author })
  else
    res.status(404).json({ message: 'Book not found!' })
}

export const createAuthor = async (req: Req, res: Res) => {
  const { name, surname, age } = req.body
  const createdAuthor = await prisma.author.create({
    data: { name, surname, age }
  })
  res.json({ createdAuthor })
}

export const updateAuthor = async (req: Req, res: Res) => {
  const authorId = Number(req.params.id)
  const updatedAuthor = await prisma.author.update({
    where: { id: authorId },
    data: req.body,
  })
  res.json({ updatedAuthor })
}

export const deleteAuthorById = async (req: Req, res: Res) => {
  const authorId = Number(req.params.id)
  const deletedAuthor = await prisma.author.delete({
    where: { id: authorId },
    include: {
      books: {
        select: {
          title: true,
          genre: true,
        }
      }
    }
  })
  if (deletedAuthor)
    res.json({ deletedAuthor })
  else
    res.status(404).json({ deletedAuthor })
}

export const createReview = async (req: Req, res: Res) => {
  const { nickname, content, book_id } = req.body
  const createdReview = await prisma.review.create({
    data: {
      nickname,
      content,
      book: {
        connect: {
          id: book_id
        }
      }
    }
  })
  res.json({ createdReview })
}

Y eso es! ahora podemos crear libros, consultarlos (podemos ver la información del autor y las reviews), actualizarlos y eliminarlos, al igual que con los autores, y podemos crear reviews para los libros.

Resumen

En éste post hemos podido apreciar como Prisma nos facilita el trabajo con la base de datos en nuesta app al abstraer esa capa y proveernos de un API intuitiva y poderosa obtenida de un análisis e introspección de nuestra BD. Como desarrolladores, ésta herramienta nos va a permitir acelerar nuestros tiempos de desarrollo a la vez que nos permite tener un mejor control y acceso a los datos que estemos manejando.
Si tienes un proyecto en Node.js con MySQL, PostgreSQL o SQLite, echale un ojo a Prisma, te va a ser muy útil!

Alt Text

Posted on May 8 by:

denispixi profile

Denis Adhemar

@denispixi

Full-Stack web & mobile developer

Discussion

markdown guide