Prisma, un toolkit para bases de datos (¿ORM?) para TypeScript y Node.js
Denis Adhemar
Posted on May 8, 2020
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!
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:
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:
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 😎:
Invocamos a nuestra API y vemos ahora sí la información de los autores 🤓:
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
:
Y que pasaría si invocamos un libro que no existe?
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!
Posted on May 8, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.