ECE1724: Advanced Web Development

React Ecosystem and Modern Frameworks

Chen Ying

Assistant Professor, Teaching Stream

Department of Electrical and Computer Engineering

University of Toronto

Last Week’s Lecture

Backend Development Fundamentals

  • HTTP: Communication protocol
  • Express: Web framework for handling requests & responses
  • Middleware: A function that sits between the request and the response
  • REST: An architectural style for designing APIs on top of HTTP
  • SQLite: A lightweight, file-based relational database

RESTful API: Example

express-example/server.js
const express = require("express");
const app = express();

app.use(express.json());

const users = [
  { id: 1, name: "Alice" },
  { id: 2, name: "Bob" },
];

app.get("/api/users", (req, res) => {
  res.json(users);
});

app.post("/api/users", (req, res) => {
  const name = req.body.name;
  if (!name) {
    return res.status(400).json({ error: "name is required" });
  }

  const newUser = {
    id: users.length + 1,
    name,
  };

  users.push(newUser);

  res.status(201).json(newUser);
});

app.listen(3000, () => {
  console.log("Server running on http://localhost:3000");
});

Data will be lost if server restarts

Use SQLite Database

express-example/server.js
const express = require("express");
const sqlite3 = require("sqlite3").verbose();

const app = express();

app.use(express.json());

const db = new sqlite3.Database("demo_db.db", (err) => {
  if (err) {
    console.error("Error connecting to database:", err);
  } else {
    console.log("Connected to SQLite database");
  }
});

db.run(
  `
    CREATE TABLE IF NOT EXISTS users (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT NOT NULL
    )
`,
  (err) => {
    if (err) {
      console.error("Error creating table:", err);
    } else {
      console.log("users table ready");
    }
  }
);

app.post("/api/users", (req, res) => {
  const name = req.body.name;
  db.run("INSERT INTO users (name) VALUES (?)", [name], function (err) {
    if (err) {
      res.status(400).json({ error: err.message });
      return;
    }
    res.status(201).json({
      id: this.lastID,
      name: name,
    });
  });
});

app.listen(3000, () => {
  console.log("Server running on http://localhost:3000");
});

Live Demo

curl -X POST http://localhost:3000/api/users \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Alice"
  }'

Potential Problems

app.post("/api/users", (req, res) => {
  const name = req.body.name;
  db.run("INSERT INTO users (name) VALUES (?)", [name], function (err) {});
});
  • Database logic inside route handler
    • As the app grows, it will become hard to read, test, reuse
  • Database operations should be asynchronous
    • Even though in Node.js, SQL queries do not have to be asynchronous

DB Operations Should Be Asynchronous

Database calls

  • Perform disk I/O
  • Can take time
  • Must not block the event loop

Node.js handles many requests concurrently

db.run("INSERT INTO users (name) VALUES (?)", [name], function (err) {});

Blocking is bad

Use async / await

Wrap database logic in a Promise

const addUser = async (name) => {
  return await new Promise((resolve, reject) => {
    db.run("INSERT INTO users (name) VALUES (?)", [name], function (err) {
      if (err) reject(err);
      else resolve({ id: this.lastID, name });
    });
  });
};

Equivalent (semantically) to

async function addUser(name) {
  ...
}

Cleaner Route

app.post("/api/users", async (req, res) => {
  try {
    const user = await addUser(req.body.name);
    res.status(201).json(user);
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});

Live Demo

server.js
const express = require("express");
const sqlite3 = require("sqlite3").verbose();

const app = express();

app.use(express.json());

const db = new sqlite3.Database("demo_db.db", (err) => {
  if (err) {
    console.error("Error connecting to database:", err);
  } else {
    console.log("Connected to SQLite database");
  }
});

db.run(
  `
    CREATE TABLE IF NOT EXISTS users (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT NOT NULL
    )
`,
  (err) => {
    if (err) {
      console.error("Error creating table:", err);
    } else {
      console.log("users table ready");
    }
  }
);

const addUser = async (name) => {
  return await new Promise((resolve, reject) => {
    db.run("INSERT INTO users (name) VALUES (?)", [name], function (err) {
      if (err) reject(err);
      else resolve({ id: this.lastID, name });
    });
  });
};

app.post("/api/users", async (req, res) => {
  try {
    const user = await addUser(req.body.name);
    res.status(201).json(user);
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});

app.listen(3000, () => {
  console.log("Server running on http://localhost:3000");
});
curl -X POST http://localhost:3000/api/users \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Charlie"
  }'

Everything Is in server.js

As the app grows

  • Routes
  • Database code
  • Validation
  • Middleware

We need separation of concerns

Modular Backend Structure

sqlite-example/
├── src/
│   ├── server.js        # Express application setup
│   ├── routes.js        # API routes
│   ├── database.js      # Database operations
│   └── middleware.js    # Custom middleware
└── package.json

Each file has one responsibility

server.js

Responsibilities:

  • Create Express app
  • Register middleware
  • Mount routes
  • Start server

server.js

sqlite-example/src/server.js
const express = require("express");
const routes = require("./routes");

const app = express();

app.use(express.json()); // Middleware to parse JSON

app.use("/api", routes); // Mount the routes

app.listen(3000, () => {
  console.log(`Server running on http://localhost:3000`);
});

routes.js

Responsibilities:

  • Define API endpoints
  • Validate input
  • Call database functions
  • Return responses

routes.js

sqlite-example/src/routes.js
const express = require("express");
const db = require("./database");

const router = express.Router();

// POST /api/users
router.post("/users", async (req, res) => {
  try {
    const { name } = req.body;
    const user = await db.addUser(name);
    res.status(201).json(user);
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});

module.exports = router;

express.Router()

const express = require("express");
const router = express.Router();

Create a new router object

  • A mini Express application that can be used to define routes separately from the main application
  • Mounted in server.js

express.Router()

sqlite-example/src/server.js
const express = require("express");
const routes = require("./routes");

const app = express();

app.use(express.json()); // Middleware to parse JSON

app.use("/api", routes); // Mount the routes

app.listen(3000, () => {
  console.log(`Server running on http://localhost:3000`);
});

express.Router(): Benefits

  • Modularity: Group related routes into a single router and mount them under a specific path

    const routes = require("./routes");
    app.use("/api", routes);
  • Readability: The main application becomes cleaner, with just a few app.use() statements to mount routers

  • Reusability: Reuse the same router across different parts of the app

routes.js

sqlite-example/src/routes.js
const express = require("express");
const db = require("./database");

const router = express.Router();

// POST /api/users
router.post("/users", async (req, res) => {
  try {
    const { name } = req.body;
    const user = await db.addUser(name);
    res.status(201).json(user);
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});

module.exports = router;

express.Router(): Benefits

Modularity: Group related routes into a single router and mount them under a specific path

Readability: The main application becomes cleaner, with just a few app.use() statements to mount routers

Reusability: Reuse the same router across different parts of the app

Middleware Usage: Attach middleware at the route level

assignment-1/src/routes.js
// GET /api/papers/:id
router.get("/papers/:id", validateId, async (req, res, next) => {})

express.Router(): Benefits

Middleware Usage: Attach middleware at the route level

  • Multiple middleware can be attached to a single route
router.get(
  "/papers/:id",
  authenticateUser,
  validateId,
  checkPermission,
  async (req, res) => {
    // final handler
  }
);

Express treats middleware as a chain

A request flows through middleware from left to right

Router-Level Middleware

Apply middleware to all routes in a router

router.use(authenticateUser);

router.get("/papers/:id", validateId, handler);
router.get("/papers", validateQueryParams, handler);
  • Everything attached with router.use() applies to all routes in that router

Express can compose many small middleware functions into a clean, readable request pipeline

express.Router(): Benefits

Modularity: Group related routes into a single router and mount them under a specific path

Readability: Keep the main application clean, with just a few app.use() statements

Reusability: Reuse the same router (and its middleware) across different parts of the app

Middleware Usage: Attach middleware at the route level

database.js

Responsibilities:

  • Connect to SQLite
  • Create tables
  • Run SQL queries
  • Return data as JavaScript objects

database.js

sqlite-example/src/database.js
const sqlite3 = require("sqlite3").verbose();

const db = new sqlite3.Database("./demo_db.db", (err) => {
  if (err) {
    console.error("Error connecting to database:", err);
  } else {
    console.log("Connected to SQLite database");
  }
});

db.run(
  `
    CREATE TABLE IF NOT EXISTS users (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT NOT NULL
    )
`,
  (err) => {
    if (err) {
      console.error("Error creating table:", err);
    } else {
      console.log("Users table ready");
    }
  }
);

const dbOperations = {
  addUser: async (name) => {
    const user = await new Promise((resolve, reject) => {
      db.run("INSERT INTO users (name) VALUES (?)", [name], function (err) {
        if (err) reject(err);
        else resolve({ id: this.lastID, name });
      });
    });
    return user;
  },
};

module.exports = {
  db,
  ...dbOperations,
};

middleware.js

Responsibilities:

  • Logging
  • Validation
  • Error handling
sqlite-example/src/middleware.js
const requestLogger = (req, res, next) => {
  console.log(`${new Date().toISOString()} - ${req.method} ${req.path}`);
  next();
};

module.exports = { requestLogger };

Modular Backend Structure

sqlite-example/
├── src/
│   ├── server.js        # Express application setup
│   ├── routes.js        # API routes
│   ├── database.js      # Database operations
│   └── middleware.js    # Custom middleware
└── package.json

Live Demo

sqlite-example/src/server.js
const express = require("express");
const routes = require("./routes");
const middleware = require("./middleware");

const app = express();

// Middleware
app.use(express.json());
app.use(middleware.requestLogger);

// Routes
app.use("/api", routes);

app.listen(3000, () => {
  console.log(`Server running on http://localhost:3000`);
});

Assignment 1

assignment-1/
├── src/
│   ├── server.js        # Express application setup
│   ├── routes.js        # API routes
│   ├── database.js      # Database operations
│   └── middleware.js    # Custom middleware
└── package.json

Today’s Lecture

PostgreSQL with Prisma ORM

TypeScript Introduction

Recap: Assignment 1

  • Express + RESTful API
  • SQLite (file‑based database)
  • JavaScript
  • async/await
  • Modular backend structure

This works — but only up to a point

Limitations of SQLite

Database is a single file

  • Limited support for concurrent writes
  • No real user / permission model
  • Not designed for multi‑service systems

SQLite is excellent for learning and prototyping

Production systems need a real database server

PostgreSQL

An open-source production‑grade relational database

  • Client–server architecture
  • Handles concurrent users
  • Strong consistency guarantees
  • Industry standard

Client-Server Architecture

  • Database is a separate system
    • It runs as a server process on a machine
  • Roles
    • Server: The PostgreSQL database process
    • Client
      • Your Express backend
      • Command-line tools like psql

Client-Server Architecture

Communication

  • Client sends SQL queries to Database Server
  • Server executes the queries and accesses Disk
  • Server returns results (rows, errors, metadata)

Communication happens over a network protocol

  • Even on your laptop, your app and PostgreSQL talk as if they were on different machines

Your Express AppPostgreSQL ServerDisk

Lifecycle

Database Server

  • Starts independently
  • Keeps running across app restarts

Application

  • Connects when needed
  • Disconnects when done

Database is not part of your Node.js process

  • This boundary is critical for systems thinking

SQLite vs. PostgreSQL

SQLite PostgreSQL
Library Server
Same process Separate process
File access Network protocol
App lifecycle Independent lifecycle

This is NOT “winner vs. loser”

SQLite and PostgreSQL solve different problems

SQLite vs. PostgreSQL

SQLite is ideal when

  • You build a small or personal application
  • Simplicity and zero setup matter
  • Low concurrency is acceptable

PostgreSQL is a better choice when

  • The system has multiple users
  • The database is queried frequently and concurrently
  • Reliability, scalability, and access control matter

Systems-Thinking Takeaway

In system and architecture design, there is no perfect choice that works best for every scenario

  • Understand multiple options

  • Know the trade-offs of each

  • Make decisions based on

    • Requirements: What the system must do
    • Constraints: Limits you can’t ignore (e.g., time, budget, tech stack, regulations, team skills)
    • Goals: What you’re optimizing for (e.g., scalability, simplicity, performance, cost, learning value)

Assignment 2

Migration to PostgreSQL + Prisma ORM

  • Replace SQLite with PostgreSQL
  • Implement a many-to-many relationship between papers and authors
    • A paper can have multiple authors
    • An author can write multiple papers
  • Query data using Prisma Client

Prisma ORM

To build this system without Prisma, you would need to

  • Manually create tables in PostgreSQL
  • Write SQL for INSERT, SELECT, UPDATE, DELETE
  • Manually map query results to JavaScript objects
  • Handle schema changes by hand

Without Prisma ORM

“Get all papers, and for each paper, get all of its authors”

  • Table 1: Paper

    id    title   publishedIn   year    ...
  • Table 2: Author

    id    name     email        affiliation   ...
  • Table 3: PaperToAuthor

    paperId       authorId
       1             2
       1             5
       2             1

Without Prisma ORM

“Get all papers, and for each paper, get all of its authors”

SELECT
  p.*,
  a.*
FROM "Paper" p
JOIN "PaperToAuthor" pa ON pa."paperId" = p.id
JOIN "Author" a ON a.id = pa."authorId"
ORDER BY p.id, a.id;

To use this result correctly, you must:

  • Group rows by paper
  • Nest authors under each paper
  • Preserve ordering
  • Handle empty cases

Raw SQL Doesn’t Scale

Raw SQL does not scale well in large applications

  • SQL queries embedded as strings
  • Runtime errors instead of compile-time errors
  • Hard to refactor safely
  • No centralized schema definition

Large systems need structure, safety, maintainability

With Prisma ORM

“Get all papers, and for each paper, get all of its authors”

prisma.paper.findMany({
  include: { authors: true }
});

Prisma ORM can

  • Generate and execute SQL
  • Reassemble rows into structured objects
  • Return clean JSON

Prisma ORM Does NOT Remove SQL

  • SQL still runs in PostgreSQL
  • Prisma ORM generates SQL for you
  • You describe structure and intent
  • Prisma ORM handles mechanics

ORM: Core Idea

Object‑Relational Mapping: Interact with a relational database using object-oriented programming language

  • Tables ↔︎ Models
  • Rows ↔︎ Objects
  • SQL ↔︎ Method calls

ORMs do not remove SQL

  • They organize access to SQL

Prisma ORM is one implementation of this idea

Why Prisma ORM?

  • Use the same workflow across popular databases
  • Describe your database structure in one place (schema.prisma)
  • Work with relational data without writing SQL
  • Safely manage schema changes (prisma migrate)
  • Build complex queries consistently and safely using a simple, type-safe API (prisma client)

Prisma ORM makes database interactions structured, predictable, and easier to reason about

Prisma ORM Architecture

Express routes call Prisma Client

app.get("/api/papers", async (req, res) => {
  const papers = await prisma.paper.findMany({
    include: { authors: true }
  });
  res.json(papers);
});

Prisma Client talks to PostgreSQL

Prisma ORM Architecture

  • Express Routes: HTTP layer (your code)
  • Prisma ORM: Database access layer (library)
  • PostgreSQL: Data storage

Two Phases of Prisma ORM

Runtime

  • Prisma Client

Design time

  • schema.prisma: Describe what the database should look like
  • Prisma Migrate: Apply schema changes to PostgreSQL

Prisma ORM Architecture

Prisma ORM Architecture

Layer Question it answers
schema.prisma What does my database look like?
Prisma Migrate How do schema changes get applied?
PostgreSQL Where is the data actually stored?
Prisma Client How do I query data in application code?
Express Routes How does this connect to my HTTP API?

Each layer has one clear responsibility

Set Up Prisma

Prerequisites

  1. Install Node.js

  2. Install PostgreSQL v17, then verify

    psql --version
  3. Start PostgreSQL server

    brew services start postgresql@17

Stop PostgreSQL server

brew services stop postgresql@17

Prerequisites

Start PostgreSQL server

  • macOS

    brew services start postgresql@17
  • Linux

    systemctl start postgresql
  • Windows: Use pgAdmin (GUI client used to connect to and manage the running PostgreSQL server)

Set Up Prisma

  1. Create a project directory and navigate into it

    mkdir prisma-example
    cd prisma-example
  2. Initialize a Node.js project

    npm init -y
    npm install typescript tsx @types/node --save-dev
    npx tsc --init

Set Up Prisma

Install required dependencies

npm install prisma @types/node @types/pg --save-dev
npm install @prisma/client @prisma/adapter-pg pg dotenv
  • @prisma/client: Prisma Client library for querying database
  • @prisma/adapter-pg: Adapter that connects Prisma Client to your database
    • Prisma Client → Adapter → PostgreSQL
  • pg: Non-blocking PostgreSQL client for Node.js
  • dotenv: Loads environment variables from your .env file

Set Up Prisma

Apply standard modern TypeScript + Node.js settings

  • Update tsconfig.json
prisma-example/tsconfig.json
{
  "compilerOptions": {
    "module": "ESNext",
    "moduleResolution": "node",
    "target": "ES2023",
    "strict": true,
    "esModuleInterop": true,
    "ignoreDeprecations": "6.0"
  }
}

Set Up Prisma

Apply standard modern TypeScript + Node.js settings

  • Update tsconfig.json
  • Update package.json
prisma-example/package.json
{
  "type": "module",
}

Initialize Prisma ORM

Invoke the Prisma CLI

npx prisma

Set up your Prisma ORM project

npx prisma init --datasource-provider postgresql --output ../generated/prisma

npx prisma init

prisma directory

  • schema.prisma file: Defines database connection and schema models

schema.prisma

Define database connection and schema models

prisma-example/prisma/schema.prisma
generator client {
  provider = "prisma-client"
  output   = "../generated/prisma"
}

datasource db {
  provider = "postgresql"
}

npx prisma init

prisma directory

  • schema.prisma file: Defines database connection and schema models

.env file: Stores environment variables

prisma-example/.env
DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"

prisma.config.ts file: Prisma configuration

prisma.config.ts

Prisma configuration

prisma-example/prisma.config.ts
import 'dotenv/config'
import { defineConfig, env } from 'prisma/config'

export default defineConfig({
  schema: 'prisma/schema.prisma',
  migrations: {
    path: 'prisma/migrations',
  },
  datasource: {
    url: env('DATABASE_URL'),
  },
})

Define Data Model

prisma-example/prisma/schema.prisma
generator client {
  provider = "prisma-client"
  output   = "../generated/prisma"
}

datasource db {
  provider = "postgresql"
}

model Book {
  id        Int      @id @default(autoincrement())
  title     String
  authors   Author[] // Many-to-many relation
  createdAt DateTime @default(now())
}

model Author {
  id    Int    @id @default(autoincrement())
  name  String
  books Book[] // Many-to-many relation
}

Create a Database

Connect to PostgreSQL server

psql postgres
  • Open a session to the default postgres database

Create a database

CREATE DATABASE mydb;

Verify

\l

Update .env

prisma-example/.env
DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"

PostgreSQL defaults to trust your OS user

psql postgres

Verify inside psql

SELECT current_user;
  • You should see your OS username

Update .env

prisma-example/.env
DATABASE_URL="postgresql://chenying@localhost:5432/mydb?schema=public"


PostgreSQL trusts your OS user

→ No password is required

This is fine for local development

Define Data Model

prisma-example/prisma/schema.prisma
generator client {
  provider = "prisma-client"
  output   = "../generated/prisma"
}

datasource db {
  provider = "postgresql"
}

model Book {
  id        Int      @id @default(autoincrement())
  title     String
  authors   Author[] // Many-to-many relation
  createdAt DateTime @default(now())
}

model Author {
  id    Int    @id @default(autoincrement())
  name  String
  books Book[] // Many-to-many relation
}

Create Database Table

npx prisma migrate dev --name init
  • Create prisma/migrations directory

    The following migration(s) have been created and applied from new schema changes:
    
    prisma/migrations/
      └─ 20260123115209_init/
        └─ migration.sql
  • Prisma handles many-to-many relationship

Relationships

In relational databases, relationships define how tables are connected to each other

  • One-to-One: One record in a table is related to one record in another table (e.g. A person has one passport)
  • One-to-Many: One record in a table is related to many records in another table (e.g. A publisher can have many books)
  • Many-to-Many: Many records in one table are related to many records in another table

Many-to-Many

Require a join table to link the two tables

CREATE TABLE book_authors (
    book_id INTEGER REFERENCES books(id) ON DELETE CASCADE,
    author_id INTEGER REFERENCES authors(id) ON DELETE CASCADE,
    PRIMARY KEY (book_id, author_id)
);
  • ON DELETE CASCADE: Ensure that if a book or an author is deleted, the corresponding rows in the book_authors table will also be deleted automatically

Prisma handles the join table automatically

Prisma Migrate

  • Generate SQL migrations
  • Apply schema changes
  • Track schema history
prisma-example/prisma/schema.prisma
model Book {
  id        Int      @id @default(autoincrement())
  title     String
  year      Int
  authors   Author[] // Many-to-many relation
  createdAt DateTime @default(now())
}

Create the second migration

npx prisma migrate dev --name added_book_year

Generate Prisma Client

npx prisma generate

Prisma Client

Generated based on schema.prisma

  • Run npx prisma generate whenever schema.prisma changes

Instantiate Prisma Client

Create a single Prisma Client instance that connects your app to PostgreSQL

prisma-example/lib/prisma.ts
import "dotenv/config";
import { PrismaPg } from '@prisma/adapter-pg'
import { PrismaClient } from '../generated/prisma/client'

const connectionString = `${process.env.DATABASE_URL}`

const adapter = new PrismaPg({ connectionString })
const prisma = new PrismaClient({ adapter })

export { prisma }
  • prisma object is how your app talks to the database

Query with Prisma

prisma-example/script.ts
import { prisma } from "./lib/prisma";

async function main() {
  try {
    // Create a new book with an author
    const book = await prisma.book.create({
      data: {
        title: "A Book",
        year: 2025,
        authors: {
          create: {
            name: "Alice",
          },
        },
      },
      include: {
        authors: true, // Return the book plus its authors in the result
      },
    });

    console.log("Created book:", book);

    // Fetch all books with their authors
    const allBooks = await prisma.book.findMany({
      include: {
        authors: true,
      },
    });

    console.log("All books:", JSON.stringify(allBooks, null, 2));
  } finally {
    await prisma.$disconnect();
  }
}

main().catch(console.error);

Live Demo

npx tsx script.ts

Query with Prisma

prisma-example/script.ts
import { prisma } from "./lib/prisma";

async function main() {
  try {
    // Create a new book with an author
    const book = await prisma.book.create({
      data: {
        title: "A Book",
        year: 2025,
        authors: {
          create: {
            name: "Alice",
          },
        },
      },
      include: {
        authors: true, // Return the book plus its authors in the result
      },
    });

    console.log("Created book:", book);

    // Fetch all books with their authors
    const allBooks = await prisma.book.findMany({
      include: {
        authors: true,
      },
    });

    console.log("All books:", JSON.stringify(allBooks, null, 2));
  } finally {
    await prisma.$disconnect();
  }
}

main().catch(console.error);

Prisma Client methods are fully typed based on your schema

TypeScript

TypeScript catches errors before runtime

Prisma Client automatically generates types that exactly match your database schema

npx prisma generate
  • Generate types in generated/prisma/models
    • These types come from schema.prisma

Prisma is fully type-safe because we are using TypeScript

TypeScript

Build up on JavaScript and extends its syntax by adding static type checking

  • Help catch type-related errors at compile time, ensuring that variables, functions, and objects are used in a type-safe way

Why TypeScript?

  • Catch errors before runtime
  • Improve code clarity
  • Standard with modern frameworks

vs. JavaScript

example.js
function add(a, b) {
  return a + b;
}

console.log(add(1, 2));
console.log(add("1", "2"));
console.log(add(1, "2"));

TypeScript

TypeScript allows for type annotations (e.g., a: number)

example.ts
function add(a: number, b: number): number {
  return a + b;
}

console.log(add(1, 2));
console.log(add("1", "2"));

Runtime bug → Compile‑time error

Type Annotations?

prisma-example/script.ts
import { prisma } from "./lib/prisma";

async function main() {
  try {
    // Create a new book with an author
    const book = await prisma.book.create({
      data: {
        title: "A Book",
        year: 2025,
        authors: {
          create: {
            name: "Alice",
          },
        },
      },
      include: {
        authors: true, // Return the book plus its authors in the result
      },
    });

    console.log("Created book:", book);

    // Fetch all books with their authors
    const allBooks = await prisma.book.findMany({
      include: {
        authors: true,
      },
    });

    console.log("All books:", JSON.stringify(allBooks, null, 2));
  } finally {
    await prisma.$disconnect();
  }
}

main().catch(console.error);
  • Hover over book or allBooks

TS: Two Ways to Get Types

  1. Write types explicitly
    • a: number
  2. Let TypeScript infer types
    • From function return values
    • From libraries like Prisma

In Prisma, the schema is the source of truth

  • TypeScript infers types from it

Lecture Summary

PostgreSQL: A client-server relational database

  • Separate process with its own lifecycle
  • Designed for multi-user, concurrent systems

Prisma ORM: A structured way to interact with PostgreSQL

  • prisma.schema defines data model
  • Prisma Migrate applys schema changes
  • Prisma Client generates safe, consistent queries

Lecture Summary

TypeScript Introduction

  • Adds static type checking to JavaScript
  • Catches errors before runtime
  • Prisma leverages TypeScript for type safety