Prisma is a popular data-mapping layer (ORM) for server-side JavaScript and TypeScript. Its core purpose is to simplify and automate how data moves between storage and application code. Prisma supports a wide range of datastores and provides a powerful yet flexible abstraction layer for data persistence. Get a feel for Prisma and some of its core features with this code-first tour.
An ORM layer for JavaScript
Object-relational mapping (ORM) was pioneered by the Hibernate framework in Java. The original goal of object-relational mapping was to overcome the so-called impedance mismatch between Java classes and RDBMS tables. From that idea grew the more broadly ambitious notion of a general-purpose persistence layer for applications. Prisma is a modern JavaScript-based evolution of the Java ORM layer.
Prisma supports a range of SQL databases and has expanded to include the NoSQL datastore, MongoDB. Regardless of the type of datastore, the overarching goal remains: to give applications a standardized framework for handling data persistence.
The domain model
We’ll use a simple domain model to look at several kinds of relationships in a data model: many-to-one, one-to-many, and many-to-many. (We’ll skip one-to-one, which is very similar to many-to-one.)
Prisma uses a model definition (a schema) that acts as the hinge between the application and the datastore. One approach when building an application, which we’ll take here, is to start with this definition and then build the code from it. Prisma automatically applies the schema to the datastore.
The Prisma model definition format is not hard to understand, and you can use a graphical tool, PrismaBuilder, to make one. Our model will support a collaborative idea-development application, so we’ll have User, Idea, and Tag models. A User
can have many Ideas
(one-to-many) and an Idea
has one User
, the owner (many-to-one). Ideas
and Tags
form a many-to-many relationship. Listing 1 shows the model definition.
Listing 1. Model definition in Prisma
datasource db {
provider = "sqlite"
url = "file:./dev.db"
}
generator client {
provider = "prisma-client-js"
}
model User {
id Int @id @default(autoincrement())
name String
email String @unique
ideas Idea[]
}
model Idea {
id Int @id @default(autoincrement())
name String
description String
owner User @relation(fields: [ownerId], references: [id])
ownerId Int
tags Tag[]
}
model Tag {
id Int @id @default(autoincrement())
name String @unique
ideas Idea[]
}
Listing 1 includes a datasource definition (a simple SQLite database that Prisma includes for development purposes) and a client definition with “generator client” set to prisma-client-js
. The latter means Prisma will produce a JavaScript client the application can use for interacting with the mapping created by the definition.
As for the model definition, notice that each model has an id
field, and we are using the Prisma @default(autoincrement())
annotation to get an automatically incremented integer ID.
To create the relationship from User
to Idea
, we reference the Idea
type with array brackets: Idea[]
. This says: give me a collection of Ideas
for the User
. On the other side of the relationship, you give Idea
a single User
with: owner User @relation(fields: [ownerId], references: [id])
.
Besides the relationships and the key ID fields, the field definitions are straightforward; String
for Strings
, and so on.
Create the project
We'll use a simple project to work with Prisma’s capabilities. The first step is to create a new Node.js project and add dependencies to it. After that, we can add the definition from Listing 1 and use it to handle data persistence with Prisma's built-in SQLite database.
To start our application, we’ll create a new directory, init an npm
project, and install the dependencies, as shown in Listing 2.
Listing 2. Create the application
mkdir iw-prisma
cd iw-prisma
npm init -y
npm install express @prisma/client body-parser
mkdir prisma
touch prisma/schema.prisma
Now, create a file at prisma/schema.prisma
and add the definition from Listing 1. Next, tell Prisma to make SQLite ready with a schema, as shown in Listing 3.
Listing 3. Set up the database
npx prisma migrate dev --name init
npx prisma migrate deploy
Listing 3 tells Prisma to “migrate” the database, which means applying schema changes from the Prisma definition to the database itself. The dev
flag tells Prisma to use the development profile, while --name
gives an arbitrary name for the change. The deploy
flag tells prisma to apply the changes.
Use the data
Now, let’s allow for creating users with a RESTful endpoint in Express.js. You can see the code for our server in Listing 4, which goes inside the iniw-prisma/server.js
file. Listing 4 is vanilla Express code, but we can do a lot of work against the database with minimal effort thanks to Prisma.
Listing 4. Express code
const express = require('express');
const bodyParser = require('body-parser');
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
const app = express();
app.use(bodyParser.json());
const port = 3000;
app.listen(port, () => {
console.log(`Server is listening on port ${port}`);
});
// Fetch all users
app.get('/users', async (req, res) => {
const users = await prisma.user.findMany();
res.json(users);
});
// Create a new user
app.post('/users', async (req, res) => {
const { name, email } = req.body;
const newUser = await prisma.user.create({ data: { name, email } });
res.status(201).json(newUser);
});
Currently, there are just two endpoints, /users GET
for getting a list of all the users, and /user POST
for adding them. You can see how easily we can use the Prisma client to handle these use cases, by calling prisma.user.findMany()
and prisma.user.create()
, respectively.
The findMany()
method without any arguments will return all the rows in the database. The create()
method accepts an object with a data field holding the values for the new row (in this case, the name and email—remember that Prisma will auto-create a unique ID for us).
Now we can run the server with: node server.js
.
Testing with CURL
Let’s test out our endpoints with CURL, as shown in Listing 5.
Listing 5. Try out the endpoints with CURL
$ curl http://localhost:3000/users
[]
$ curl -X POST -H "Content-Type: application/json" -d '{"name":"George Harrison","email":"george.harrison@example.com"}' http://localhost:3000/users
{"id":2,"name":"John Doe","email":"john.doe@example.com"}{"id":3,"name":"John Lennon","email":"john.lennon@example.com"}{"id":4,"name":"George Harrison","email":"george.harrison@example.com"}
$ curl http://localhost:3000/users
[{"id":2,"name":"John Doe","email":"john.doe@example.com"},{"id":3,"name":"John Lennon","email":"john.lennon@example.com"},{"id":4,"name":"George Harrison","email":"george.harrison@example.com"}]
Listing 5 shows us getting all users and finding an empty set, followed by adding users, then getting the populated set.
Next, let’s add an endpoint that lets us create ideas and use them in relation to users, as in Listing 6.
Listing 6. User ideas POST endpoint
app.post('/users/:userId/ideas', async (req, res) => {
const { userId } = req.params;
const { name, description } = req.body;
try {
const user = await prisma.user.findUnique({ where: { id: parseInt(userId) } });
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
const idea = await prisma.idea.create({
data: {
name,
description,
owner: { connect: { id: user.id } },
},
});
res.json(idea);
} catch (error) {
console.error('Error adding idea:', error);
res.status(500).json({ error: 'An error occurred while adding the idea' });
}
});
app.get('/userideas/:id', async (req, res) => {
const { id } = req.params;
const user = await prisma.user.findUnique({
where: { id: parseInt(id) },
include: {
ideas: true,
},
});
if (!user) {
return res.status(404).json({ message: 'User not found' });
}
res.json(user);
});
In Listing 6, we have two endpoints. The first allows for adding an idea using a POST
at /users/:userId/ideas
. The first thing it needs to do is recover the user by ID, using prisma.user.findUnique()
. This method is used for finding a single entity in the database, based on the passed-in criteria. In our case, we want the user with the ID from the request, so we use: { where: { id: parseInt(userId) } }
.
Once we have the user, we use prisma.idea.create
to create a new idea
. This works just like when we created the user
, but we now have a relationship field. Prisma lets us create the association between the new idea
and user
with: owner: { connect: { id: user.id } }
.
The second endpoint is a GET
at /userideas/:id
. The purpose of this endpoint is to take the user ID and return the user including their ideas. This gives us a look at the where
clause in use with the findUnique
call, as well as the include
modifier. The modifier is used here to tell Prisma to include the associated ideas. Without this, the ideas would not be included, because Prisma by default uses a lazy loading fetch strategy for associations.
To test the new endpoints, we can use the CURL commands shown in Listing 7.
Listing 7. CURL for testing endpoints
$ curl -X POST -H "Content-Type: application/json" -d '{"name":"New Idea", "description":"Idea description"}' http://localhost:3000/users/3/ideas
$ curl http://localhost:3000/userideas/3
{"id":3,"name":"John Lennon","email":"john.lennon@example.com","ideas":[{"id":1,"name":"New Idea","description":"Idea description","ownerId":3},{"id":2,"name":"New Idea","description":"Idea description","ownerId":3}]}
We are able to add ideas and recover users with them.
Many-to-many with tags
Now let’s add endpoints for handling tags within the many-to-many relationship. In Listing 8, we handle tag creation and associate a tag
and an idea
.
Listing 8. Adding and displaying tags
// create a tag
app.post('/tags', async (req, res) => {
const { name } = req.body;
try {
const tag = await prisma.tag.create({
data: {
name,
},
});
res.json(tag);
} catch (error) {
console.error('Error adding tag:', error);
res.status(500).json({ error: 'An error occurred while adding the tag' });
}
});
// Associate a tag with an idea
app.post('/ideas/:ideaId/tags/:tagId', async (req, res) => {
const { ideaId, tagId } = req.params;
try {
const idea = await prisma.idea.findUnique({ where: { id: parseInt(ideaId) } });
if (!idea) {
return res.status(404).json({ error: 'Idea not found' });
}
const tag = await prisma.tag.findUnique({ where: { id: parseInt(tagId) } });
if (!tag) {
return res.status(404).json({ error: 'Tag not found' });
}
const updatedIdea = await prisma.idea.update({
where: { id: parseInt(ideaId) },
data: {
tags: {
connect: { id: tag.id },
},
},
});
res.json(updatedIdea);
} catch (error) {
console.error('Error associating tag with idea:', error);
res.status(500).json({ error: 'An error occurred while associating the tag with the idea' });
}
});
We've added two endpoints. The POST
endpoint, used for adding a tag, is familiar from the previous examples. In Listing 8, we've also added the POST
endpoint for associating an idea with a tag.
To associate an idea and a tag, we utilize the many-to-many mapping from the model definition. We grab the Idea
and Tag
by ID and use the connect
field to set them on one another. Now, the Idea
has the Tag
ID in its set of tags and vice versa. The many-to-many association allows up to two one-to-many relationships, with each entity pointing to the other. In the datastore, this requires creating a “lookup table” (or cross-reference table), but Prisma handles that for us. We only need to interact with the entities themselves.
The last step for our many-to-many feature is to allow finding Ideas by Tag
and finding the Tags
on an Idea
. You can see this part of the model in Listing 9. (Note that I have removed some error handling for brevity.)