React Ecosystem and Modern Frameworks
Chen Ying
Assistant Professor, Teaching Stream
Department of Electrical and Computer Engineering
University of Toronto
Backend Development Fundamentals
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
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");
});Database calls
Node.js handles many requests concurrently
Blocking is bad
async / awaitWrap database logic in a Promise
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");
});server.jsAs the app grows
We need separation of concerns
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.jsResponsibilities:
server.jssqlite-example/src/server.js
routes.jsResponsibilities:
routes.jssqlite-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()Create a new router object
server.jsexpress.Router()sqlite-example/src/server.js
express.Router(): BenefitsModularity: 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
routes.jssqlite-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(): BenefitsModularity: 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
express.Router(): BenefitsMiddleware Usage: Attach middleware at the route level
Express treats middleware as a chain
A request flows through middleware from left to right
Apply middleware to all routes in a router
router.use() applies to all routes in that routerExpress can compose many small middleware functions into a clean, readable request pipeline
express.Router(): BenefitsModularity: 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.jsResponsibilities:
database.jssqlite-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,
};Responsibilities:
sqlite-example/
├── src/
│ ├── server.js # Express application setup
│ ├── routes.js # API routes
│ ├── database.js # Database operations
│ └── middleware.js # Custom middleware
└── package.json
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/
├── src/
│ ├── server.js # Express application setup
│ ├── routes.js # API routes
│ ├── database.js # Database operations
│ └── middleware.js # Custom middleware
└── package.json
PostgreSQL with Prisma ORM
TypeScript Introduction
async/awaitThis works — but only up to a point
Database is a single file
SQLite is excellent for learning and prototyping
Production systems need a real database server
An open-source production‑grade relational database
psqlCommunication
Communication happens over a network protocol
Your Express App → PostgreSQL Server → Disk
Database Server
Application
Database is not part of your Node.js process
| 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 is ideal when
PostgreSQL is a better choice when
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
Migration to PostgreSQL + Prisma ORM
To build this system without Prisma, you would need to
INSERT, SELECT, UPDATE, DELETE“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“Get all papers, and for each paper, get all of its authors”
To use this result correctly, you must:
Raw SQL does not scale well in large applications
Large systems need structure, safety, maintainability
“Get all papers, and for each paper, get all of its authors”
Prisma ORM can
Object‑Relational Mapping: Interact with a relational database using object-oriented programming language
ORMs do not remove SQL
Prisma ORM is one implementation of this idea
schema.prisma)prisma migrate)prisma client)Prisma ORM makes database interactions structured, predictable, and easier to reason about
Express routes call Prisma Client
Prisma Client talks to PostgreSQL
Runtime
Design time
schema.prisma: Describe what the database should look like| 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
Prerequisites
Install Node.js
Install PostgreSQL v17, then verify
Start PostgreSQL server
Start PostgreSQL server
macOS
Linux
Windows: Use pgAdmin (GUI client used to connect to and manage the running PostgreSQL server)
Create a project directory and navigate into it
Initialize a Node.js project
Install required dependencies
@prisma/client: Prisma Client library for querying database@prisma/adapter-pg: Adapter that connects Prisma Client to your database
pg: Non-blocking PostgreSQL client for Node.jsdotenv: Loads environment variables from your .env fileApply standard modern TypeScript + Node.js settings
tsconfig.jsonApply standard modern TypeScript + Node.js settings
tsconfig.jsonpackage.jsonInvoke the Prisma CLI
npx prisma initprisma directory
schema.prisma file: Defines database connection and schema modelsschema.prismaDefine database connection and schema models
npx prisma initprisma 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.tsPrisma configuration
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
}.envprisma-example/.env
DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"
.envPostgreSQL trusts your OS user
→ No password is required
This is fine for local development
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 prisma/migrations directory
The following migration(s) have been created and applied from new schema changes:
prisma/migrations/
└─ 20260123115209_init/
└─ migration.sqlPrisma handles many-to-many relationship
In relational databases, relationships define how tables are connected to each other
Require a join table to link the two tables
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 automaticallyPrisma handles the join table automatically
Generated based on schema.prisma
npx prisma generate whenever schema.prisma changesCreate a single Prisma Client instance that connects your app to PostgreSQL
prisma-example/lib/prisma.ts
prisma object is how your app talks to the databaseprisma-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-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 catches errors before runtime
Prisma Client automatically generates types that exactly match your database schema
generated/prisma/models
schema.prismaPrisma is fully type-safe because we are using TypeScript
Build up on JavaScript and extends its syntax by adding static type checking
Why TypeScript?
TypeScript allows for type annotations (e.g., a: number)
example.ts
Runtime bug → Compile‑time error
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);book or allBooksa: numberIn Prisma, the schema is the source of truth
PostgreSQL: A client-server relational database
Prisma ORM: A structured way to interact with PostgreSQL
prisma.schema defines data modelTypeScript Introduction