Prisma, un toolkit para bases de datos (¿ORM?) para TypeScript y Node.js
Tags: node, javascript, typescript, database
Published: 2020-05-08 03:10:10

[Prisma](https://prisma.io) 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í](/file-utilities/prisma_example.db).

Lo primero que haremos será crear un folder para nuestro proyecto, movernos dentro de éste e inicializar un proyecto de JavaScript:
```shell
$ mkdir prisma-example && cd prisma-example && npm init -y
```
Ahora instalaremos algunas dependencias:
```shell
$ 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:
```js
{
  "compilerOptions": {
    "sourceMap": true,
    "outDir": "dist",
    "strict": true,
    "lib": ["esnext"],
    "esModuleInterop": true
  }
}
```
Y reemplazamos la parte de `"scripts"` del archivo `package.json` con lo siguiente:
```javascript
{
    //...
    "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:
```shell
$ 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](https://github.com/motdotla/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:
```js
datasource sqlite {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}
```
Y reemplazamos el contenido del archivo `.env` con lo siguiente:
```js
DATABASE_URL="file:./prisma_example.db"
```

La base de datos SQLite que hemos descargado tiene las sgtes 3 tablas ya creadas:
```sql
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:
```shell
$ 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:
```groovy
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:
```groovy
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`
```shell
$ npm i @prisma/client
```
y luego generamos nuestro cliente con el comando:
```shell
$ 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:
```shell
$ 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:
```ts
// 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:
```ts
// 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:
```shell
$ 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](https://dev-to-uploads.s3.amazonaws.com/i/8qagefo78cjai6gjbzkm.png)

## 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:
```ts
// 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 :
```ts
// 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](https://dev-to-uploads.s3.amazonaws.com/i/p4kiwzs94olfl5bndvmt.png)
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/0v2zqjs8ouio3najaba0.png)

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](https://dev-to-uploads.s3.amazonaws.com/i/jbq5mffaq4fajibo96dz.png)
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/4ax52boghwfzgfic7pd7.png)


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:
```ts
// 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](https://dev-to-uploads.s3.amazonaws.com/i/vx93iixppr0j6s1aea2c.png)

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

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/vkfjvuwml2ih6w8o07ox.png)

Podemos buscar también un libro específico por ID, para ello vamos a modificar nuestra función `getBookById` de la siguiente manera:
```ts
// 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](https://dev-to-uploads.s3.amazonaws.com/i/c6t7wrpxm21dh7wbi2lx.png)

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

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/9e71uoowqa5264rwl2sb.png)

Ahora creemos un libro 🤓. Modificamos nuestra función `createBook`:
```ts
// 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:
```ts
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](https://dev-to-uploads.s3.amazonaws.com/i/10jjs91oxzzlvaa0qzlh.png)