01. Getting Started and Core Concepts
Prisma Learning Notes
This project is a small TypeScript + Prisma + PostgreSQL practice project.
Current models:
UserTodo- One user can have many todos.
- Each todo belongs to one user.
Use this README as your Prisma revision notebook.
Current Project State
This section explains what already exists in this project.
Existing Tech Stack
Node.js Runs the project
TypeScript Lets you write typed JavaScript
Prisma ORM for database queries
PostgreSQL Database provider
Existing Package Script
In package.json, this script already exists:
{
"scripts": {
"dev": "tsc -b && node ./dist/index.js"
}
}
Meaning:
tsc -bcompiles TypeScript into JavaScript.node ./dist/index.jsruns the compiled code.- Run it with:
npm run dev
Existing Prisma Generator
In prisma/schema.prisma:
generator client {
provider = "prisma-client-js"
}
Meaning:
- Prisma generates a JavaScript/TypeScript client.
- That generated client is used through
@prisma/client. - This is why
src/index.tscan use:
import { PrismaClient } from "@prisma/client";
Existing Datasource
In prisma/schema.prisma:
datasource db {
provider = "postgresql"
}
Meaning:
- Your project is using PostgreSQL.
- The database connection URL is loaded from
prisma.config.ts.
In prisma.config.ts, this already exists:
datasource: {
url: env("DATABASE_URL"),
}
Meaning:
- Prisma reads
DATABASE_URLfrom your environment. DATABASE_URLis the connection string for your database.
Existing User Model
model User{
id Int @default(autoincrement()) @id
username String @unique
password String
age Int
city String
todos Todo[]
}
Meaning:
idis the primary key.idis automatically increased:1,2,3, and so on.usernamemust be unique.passwordstores the user's password.agestores a number.citystores text.todosmeans one user can have many todos.
Security note:
- Plain text passwords are okay only in this learning project.
- Real apps must hash passwords before saving them.
- Common choices are
bcryptorargon2.
Current learning point:
- You already know how to create a model.
- Next, learn how each field maps to a database column.
Existing Todo Model
model Todo{
id Int @default(autoincrement()) @id
title String
description String
done Boolean
userId Int
time DateTime @default(now())
user User @relation(fields:[userId],references:[id])
}
Meaning:
idis the primary key.titlestores the todo title.descriptionstores more detail.donestorestrueorfalse.userIdstores the id of the user who owns this todo.timeautomatically stores the creation time.userconnects this todo to theUsermodel.
Current learning point:
- You already have a one-to-many relation.
- One
Usercan have manyTodorecords. - Each
Todobelongs to oneUser.
Existing Relation
The relation is made from both sides:
// User side
todos Todo[]
// Todo side
userId Int
user User @relation(fields: [userId], references: [id])
Meaning:
Todo.userIdstores the foreign key.Todo.usertells Prisma how to join Todo to User.User.todoslets Prisma fetch all todos for a user.
Mental picture:
User
id = 1
username = "utsav"
Todo
id = 1
title = "Learn Prisma"
userId = 1
Here, Todo.userId = 1 points to User.id = 1.
Existing Migrations
Your project already has migrations inside:
prisma/migrations/
Migrations are the history of your database changes.
From your project, these migration ideas already exist:
initialize_prisma
remigrated_and_added_city_column
todo_model
added_date_time_in_todos_table
Meaning:
- You initialized Prisma.
- You added or changed the
Usermodel. - You added the
Todomodel. - You added a date/time field to todos.
Current learning point:
- Open each
migration.sqlfile later. - Read the SQL and compare it with
schema.prisma. - This teaches how Prisma schema becomes real database tables.
Existing src/index.ts
Your current code:
import { PrismaClient } from "@prisma/client";
const client = new PrismaClient();
async function createUser(){
await client.user.create({
data: {
username: "utsav",
password: "jfij",
age: 34,
city: "lucknow"
}
})
}
createUser();
Meaning:
- You imported Prisma Client.
- You created a Prisma Client instance.
- You wrote a
createUserfunction. - You inserted one user into the database.
Important issue:
usernameis unique.- Running this code again with
username: "utsav"can fail because that username may already exist. - The example password is plain text only because this project is focused on Prisma basics.
- In a real app, never store plain text passwords.
Better practice version:
import { PrismaClient } from "@prisma/client";
const client = new PrismaClient();
async function main() {
const user = await client.user.upsert({
where: {
username: "utsav",
},
update: {
age: 34,
city: "lucknow",
},
create: {
username: "utsav",
password: "jfij",
age: 34,
city: "lucknow",
},
});
console.log(user);
}
main()
.catch((error) => {
console.error(error);
})
.finally(async () => {
await client.$disconnect();
});
Why this is better:
upsertavoids duplicate username errors..catch()shows errors.$disconnect()closes the database connection.
What You Have Already Learned
You already touched these Prisma topics:
- Installing Prisma
- Running
npx prisma init - Creating
schema.prisma - Configuring a Prisma Client generator
- Choosing PostgreSQL as a datasource
- Connecting PostgreSQL
- Creating models
- Using
@id - Using
@default - Using
@unique - Creating a relation
- Running migrations
- Importing
PrismaClient - Creating a row with
create
What Is Still Missing For Practice
You still need to practice:
findManyfindUniquefindFirstupdatedeleteupsertincludeselect- nested writes
- filtering
- sorting
- pagination
- transactions
- Prisma Studio
- seeding
- indexes
- enums
1. What Prisma Does
Prisma helps your TypeScript code talk to your database.
Instead of writing SQL like this:
SELECT * FROM "User";
You write TypeScript like this:
await client.user.findMany();
Prisma gives you:
- A schema file:
prisma/schema.prisma - Migrations: database change history
- Prisma Client: TypeScript code for querying the database
- Prisma Studio: browser UI to see and edit data
2. Important Files
prisma/schema.prisma Defines database models
prisma/migrations/ Stores database migration history
prisma.config.ts Loads Prisma config and DATABASE_URL
src/index.ts Place to write Prisma Client practice code
package.json Contains scripts and dependencies
3. Prisma Schema Basics
Your schema currently has this generator:
generator client {
provider = "prisma-client-js"
}
Meaning:
- Generate Prisma Client for JavaScript/TypeScript.
- This lets you import
PrismaClientfrom@prisma/client.
Is This Generator Correct and Up To Date?
Yes, this is correct for the Prisma 6 setup currently used by this project:
generator client {
provider = "prisma-client-js"
}
It matches the existing import in src/index.ts:
import { PrismaClient } from "@prisma/client";
prisma-client-js is still supported, so do not change it just because it looks old.
For new Prisma 7 projects, Prisma recommends the newer ESM-first generator:
generator client {
provider = "prisma-client"
output = "../generated/prisma"
}
That newer generator requires an output path and changes the import path in your TypeScript code. For example:
import { PrismaClient } from "../generated/prisma/client";
Keep prisma-client-js while learning with this project. Later, make a separate practice branch to try prisma-client; do not mix the new generator with the old @prisma/client import.
Your datasource:
datasource db {
provider = "postgresql"
}
Meaning:
- The database is PostgreSQL.
- The database URL is currently loaded from
prisma.config.ts.
This is also correct. The datasource name db is a common convention; it could have another name, but db is clear and should stay as it is.
Your connection URL is intentionally not inside schema.prisma because prisma.config.ts provides it instead:
datasource: {
url: env("DATABASE_URL"),
}
Because prisma.config.ts calls env("DATABASE_URL"), Prisma CLI commands need this variable to be set before Prisma can load the config. Put a real PostgreSQL connection string in .env:
DATABASE_URL="postgresql://USER:PASSWORD@localhost:5432/DATABASE_NAME?schema=public"
In many Prisma projects, you may also see:
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
4. Your Models
model User {
id Int @default(autoincrement()) @id
username String @unique
password String
age Int
city String
todos Todo[]
}
model Todo {
id Int @default(autoincrement()) @id
title String
description String
done Boolean
userId Int
time DateTime @default(now())
user User @relation(fields: [userId], references: [id])
}
Meaning:
User.idis the primary key.usernamemust be unique.User.todosmeans one user has many todos.Todo.userIdstores the related user's id.Todo.userdefines the relation.
5. Useful Schema Attributes
@id
Marks a field as the primary key.
id Int @id @default(autoincrement())
@default
Gives a field a default value.
done Boolean @default(false)
@unique
Prevents duplicate values.
username String @unique
@relation
Connects two models.
user User @relation(fields: [userId], references: [id])
@updatedAt
Automatically updates a timestamp whenever the row changes.
updatedAt DateTime @updatedAt
Optional field with ?
Allows a field to be empty.
description String?
Model-level index
Helps speed up searches.
@@index([userId])
Compound unique constraint
Makes a combination unique.
@@unique([username, city])
6. Recommended Improved Schema For Learning
You can later change your models to this:
model User {
id Int @id @default(autoincrement())
username String @unique
password String
age Int
city String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
todos Todo[]
@@index([city])
}
model Todo {
id Int @id @default(autoincrement())
title String
description String?
done Boolean @default(false)
userId Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id])
@@index([userId])
}
After changing schema, run:
npx prisma migrate dev --name improve_user_todo_models