In this tutorial, you will get a holistic and practical introduction to the Prisma 2 ecosystem. This includes using Lift for database migrations and Photon JS for type-safe database access.
Note: If you encounter any problems with this tutorial or any parts of Prisma 2, please make sure to create an issue here! You can also join the
#prisma-preview
channel on Slack to share your feedback directly.
This tutorial will teach you how to:
- Install the Prisma 2 CLI
- Use the
init
command to set up a new project - Migrate your database schema using the
lift
subcommand - Generate Photon JS, a type-safe database client for JavaScript and TypeScript
- Use the
dev
command for development - Explore Photon's relation API
We will start from scratch and use TypeScript with a PostgreSQL database in this tutorial. You can set up your PostgreSQL database locally or using a hosting provider such as Heroku or Digital Ocean.
Note: If you don't want to set up a PostgreSQL database, you can still follow along by choosing SQLite in the beginning. One of Prisma's main benefits is that it lets you easily swap out the data sources your application connects to. So, while you can start with SQLite, mapping the same setup to PostgreSQL later on can be done by simply adjusting a few lines in your Prisma schema file.
The Prisma 2 CLI is available as the prisma2
package on npm. Install it globally on your machine with the following command:
npm install -g prisma2
The init
command of the Prisma 2 CLI helps you set up a new project and connect to a database. Run it as follows:
prisma2 init hello-prisma2
This launches an interactive wizard to help you with your set up, follow the steps below.
Note that the init
flow for SQLite currently slightly differs from the one for PostgreSQL. Exapnd below if you want to use SQLite.
Expand if you want to use SQLite.
In the interactive prompt, select the following:
- Select SQLite
- Check both Photon and Lift
- Select Create
- Select TypeScript
- Select From scratch
This already downloads and installs some basic boilerplate for you. Since we're starting without boilerplate in the PostgreSQL tutorial, delete everything except for the schema.prisma
file inside the prisma
directory to be in the same state as the PostgreSQL tutorial. Your folder structure should look as follows:
hello-prisma2
βββ prisma
βββ schema.prisma
- Select PostgreSQL (or use SQLite if you don't have a PostgreSQL database)
- Provide your database credentials:
- Host: IP address or domain where your PostgreSQL server is running
- Port: Port on which your PostgreSQL server is listening
- User and Password: Credentials of a database user
- Database: Name of the PostgreSQL database you want to use
- SSL: Check the box if you PostgreSQL server uses SSL (likely yes if you're not running locally)
- Select Connect
Note: This screenshot shows the configuration of a database hosted on Heroku.
- Enter the name of a new schema
- Select Introspect
Note: While the Prisma 2 CLI does offer introspection as a feature when you're getting started with an already existing database ("brownfield"), the CLI doesn't actually introspect a schema here because we're starting from scratch. This has been reported as an issue here and will be fixed very soon.
The init
flow hasn't done anything but create the following directory structure:
hello-prisma2
βββ prisma
βββ schema.prisma
Note: If you were using SQLite, make sure that you deleted all files that were generated initially and only keep the
prisma/schema.prisma
file!
schema.prisma
is your Prisma schema file. It generally contains three important elements for your project:
- Data sources (here, that's your PostgreSQL database)
- Generators (you'll add this soon)
- Data model definition (you'll add this soon)
Your Prisma schema file currently has the following contents:
datasource db {
provider = "postgresql"
url = "postgresql://USER:PASSWORD@HOST:PORT/DATABASE?schema=SCHEMA"
}
The uppercase letters are placeholders representing your database credentials.
For a PostgreSQL database hosted on Heroku, the connection string might look as follows:
datasource db {
provider = "postgresql"
url = "postgresql://opnmyfngbknppm:[email protected]:5432/d50rgmkqi2ipus?schema=hello-prisma2"
}
When running PostgreSQL locally, your user and password as well as the database name typically correspond to the current user of your OS, e.g.:
datasource db {
provider = "postgresql"
url = "postgresql://johndoe:johndoe@localhost:5432/johndoe?schema=hello-prisma2"
}
Note that it's also possible to provision the url
as an environment variable.
The data model definition inside the schema file has the following responsibilities:
- It's a declarative description of your underlying database schema
- It provides the foundation for the generated Photon API
Its main building blocks are models which map to tables in the underlying PostgreSQL database. The fields of a model map to columns of a table.
Let's go ahead and define a simple User
model, add the following to your schema file:
model User {
id String @id @default(cuid())
name String?
email String @unique
}
This defines a model User
with three fields:
- The
id
field is of typeString
and annotated with two attributes:@id
: Indicates that this field is used as the primary key@default(cuid())
: Sets a default value for the field by generating acuid
- The
name
field is of typeString?
(read: "optional string"). The?
is a type modifier expressing that this field is optional. - The
email
field is of typeString
. It is annotated with the@unique
attribute which means that there can never be two records in the database with the same value for that field. This will be enforced by Prisma.
When migrating your database based on this data model, Lift will map model and field names to table and column names. If you want to change the naming in the underlying database, you can use the @@map
block attribute to specify a different table name, and the @map
field attribute to specify a different column name. Expand below for an example.
Expand to see an example of @@map
and @map
.
With the following model definition, the table in the underlying database will be called users
and the column that maps to the name
field is called username
:
model User {
id String @id @default(cuid())
name String? @map("username")
email String @unique
@@map("users")
}
Here is what the SQL statement looks like that's generated by Lift when the migration is performed:
CREATE TABLE "hello-prisma2"."users" (
"id" text NOT NULL,
"username" text,
"email" text NOT NULL DEFAULT ''::text,
PRIMARY KEY ("id")
);
Every schema migration with Lift follows a 3-step-process:
- Adjust data model: Change your data model definition to match your desired database schema.
- Save migration: Run
prisma2 lift save
to create your migration files on the file system. - Run migration: Run
prisma2 lift up
to perform the migration against your database.
Step 1 is what you just did in the previous section, so now you need to use Lift to map the data model to your database schema.
With Lift, every database migration gets persisted on your file system, represented by a number of files. This lets developers keep a migration history of their database and understand how their project evolves over time. It also enables rolling back and "replaying" migrations.
Note: Lift also creates a table called
_Migration
in your database that additionally stores the details of every migration.
Run the following command to save your migrations files:
prisma2 lift save --name 'init'
This creates a new folder called migrations
, including your first set of migration files:
hello-prisma2
βββ prisma
βββ migrations
β βββ 20190703131441-init
β β βββ README.md
β β βββ datamodel.prisma
β β βββ steps.json
β βββ lift.lock
βββ schema.prisma
Note that the --name
option that was passed to prisma2 lift save
determines the name of the generated migration directory. To ensure uniqueness and retain order, the name of a migration directory is always prefixed with a timestamp, so in this case the migration directory is called 20190703131441-init
.
Feel free to explore the contents of each file to get a better understanding of their use.
Once the migration files are created, you can run the migration with the following command:
prisma2 lift up
This maps your data model to the underlying database schema (i.e. it migrates your database). In this case it created the following table:
CREATE TABLE "hello-prisma2"."User" (
"id" text NOT NULL,
"name" text,
"email" text NOT NULL DEFAULT ''::text,
PRIMARY KEY ("id")
);
That's it! You're now ready to access your database programmatically using Photon JS.
Photon JS is a type-safe database client for Node.js and TypeScript. It's generated from your Prisma schema file and provides an ergonomic data access API with CRUD and other operations for your data model. You can learn more about Photon's generated API here.
To generate Photon, you first need to add a generator
to your schema file. Go ahead and adjust your schema.prisma
to look as follows:
datasource db {
provider = "postgresql"
url = "postgresql://USER:PASSWORD@HOST:PORT/DATABASE?schema=SCHEMA"
}
+ generator photonjs {
+ provider = "photonjs"
+ }
model User {
id String @id @default(cuid())
name String?
email String @unique
}
With the generator
in place, run the following command to generate Photon JS:
prisma2 generate
This creates a node_modules
directory in the root directory of your project:
βββ node_modules
β βββ @generated
β βββ photon
β βββ runtime
β βββ dist
β βββ utils
βββ prisma
βββ migrations
βββ 20190703131441-init
You can also add the output
field to the generator
block to specify the file path where Photon JS should be generated. Since you're not explicitly specifying the output
here, it uses the default path which is the project's node_modules
directory. Learn more about the specifics of generating Photon into node_modules
here.
Having Photon JS located inside node_modules/@generated
enables you to import it in your code as follows:
import Photon from '@generated/photon'
or
const Photon = require('@generated/photon')
Let's set up a basic TypeScript app next so that you can start using Photon in code. To do so, run the following commands:
npm init -y
npm install --save-dev typescript ts-node
touch tsconfig.json
touch index.ts
Then add the following contents into tsconfig.json
:
{
"compilerOptions": {
"sourceMap": true,
"outDir": "dist",
"lib": ["esnext", "dom"],
"strict": true
}
}
Next, add a start
script to your package.json
:
{
"name": "local-postgres-test-2",
"version": "1.0.0",
"description": "",
"main": "index.js",
"dependencies": {},
"devDependencies": {
"ts-node": "^8.3.0",
"typescript": "^3.5.2"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
+ "start": "ts-node index.ts"
},
"keywords": [],
"author": "",
"license": "ISC"
}
Because Photon JS is generated into node_modules
which is typically populated by invoking npm install
, you should make sure that Photon JS is also generated upon every invocation of npm install
. You can do so by adding a postinstall
script to your package.json
:
{
"name": "local-postgres-test-2",
"version": "1.0.0",
"description": "",
"main": "index.js",
"dependencies": {},
"devDependencies": {
"ts-node": "^8.3.0",
"typescript": "^3.5.2"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "ts-node index.ts",
+ "postinstall": "prisma2 generate"
},
"keywords": [],
"author": "",
"license": "ISC"
}
When collaborating on a project that uses Photon JS, this approach allows for conventional Node.js best practices where a team member can clone a Git repository and then run npm install
to get their version of the Node dependencies inside their local node_modules
directory.
That's it! Let's now explore how you can use Photon inside index.ts
to read and write data in the database.
Add the following code to index.ts
:
import Photon from '@generated/photon'
const photon = new Photon()
async function main() {
// Open connection to database
await photon.connect()
// You'll write your Photon code here
// Close connection to database
await photon.disconnect()
}
main()
Photon's operations are asynchronous, this is why we want to execute them in an async
function (so that we can await
the result of the operation).
Add the following operations inside the `main function:
async function main() {
// Open connection to database
await photon.connect()
const newUser = await photon.users.create({
data: {
name: 'Alice',
email: '[email protected]'
}
})
console.log(newUser)
const allUsers = await photon.users.findMany()
console.log(allUsers)
// Close connection to database
await photon.disconnect()
}
Instead of copying and pasting the code above, try typing the operations and let yourself be guided by the autocompletion in your editor:
You can now run the script with the following command:
npm start
This first creates a new User
record in the database and subsequently fetches all users to print them in the console.
Prisma 2 features a development mode that allows for faster iterations during development. When running in development mode, the Prisma 2 CLI watches your schema file. Whenever you then save a change to the schema file, the Prisma CLI takes care of:
- (re)generating Photon
- updating your database schema
- creating a Prisma Studio endpoint for you
Note that your database schema gets updated "on the fly" without persisting a migration folder on the file system. Once you're happy with the changes you made to your data model to develop a certain feature, you can exit the development mode and actually persist your migration. Learn more here.
Launch the development mode with this command:
prisma2 dev
Note: You can stop the development mode by hitting CTRL+C two times.
Here is what the terminal screen now looks like:
Let's now evolve the application while running the development mode. Add another model to your schema.prisma
:
model Post {
id String @id @default(cuid())
title String
published Boolean @default(false)
}
Be sure to save the file. You can then observe your terminal window to see Prisma's activity:
- It added a
Post
table to your database schema - It regenerated the Photon API to add CRUD operations for the new
Post
model
To validate that this worked, you can update the code inside your main
function in index.ts
as follows:
async function main() {
// Open connection to database
await photon.connect()
const newPost = await photon.posts.create({
data: { title: 'Hello Prisma 2' }
})
console.log(newPost)
const allPosts = await photon.posts.findMany()
console.log(allPosts)
// Close connection to database
await photon.disconnect()
}
Execute the updated script:
npm run start
This now creates a new Post
record in the database.
You can explore the current content of your database using Prisma Studio. Open the endpoint that's shown in your terminal where prisma dev
is running (in most cases this will be http://localhost:5555/
).
You already have two models in your data model definition, let's now connect these via a relation:
- One post should have at most one author
- One author should have zero or more posts
To reflect these requirements, adjust your data model as follows:
model User {
id String @id @default(cuid())
name String?
email String @unique
+ posts Post[]
}
model Post {
id String @id @default(cuid())
title String
published Boolean @default(true)
+ author User?
}
Again, be sure to save the file to let Prisma update your database schema and regenerate the Photon API.
Terminate the development mode by hitting CTRL+C two times.
You've introduced two changes to your data model that are already reflected in the database and in your Photon API thanks to prisma dev
. To persists your migration in Lift's migration history, you need to run through the familiar process of migrating your database with Lift:
prisma2 lift save --name 'add-post'
prisma2 lift up
Coming soon. In the meantime, you can learn more about Photon's relations API here.