এক্সপ্রেস নিয়ে এর আগের লেখা গুলো পড়ে আসলে এই লেখা বুঝা সহজ হবে।
- এক্সপ্রেস জেএস ব্যবহার করে সিম্পল CRUD API তৈরি
- এক্সপ্রেস এপিআই অথেনটিকেশন
- এক্সপ্রেসে মাল্টি টেন্যান্ট এপিআই তৈরি
ORM হচ্ছে অবজেক্ট রিলেশনাল ম্যাপিং। আমরা যখন কোন অবজেক্ট (মডেল) তৈরি করি, তখন তার ফিল্ড গুলো সরাসরি ডেটাবেজের সাথে ম্যাপ করে। যেমন ইউজার অবজেক্টের কথা চিন্তা করি, ইউজার অবজেক্টে username এবং পাসওয়ার্ড আছে। তো আমরা যখন কোন ডেটা ইউজার টেবিলে রাখব, বা ডেটা ইউজার টেবিল থেকে রিড করব, তার জন্য সরাসরি SQL কমান্ড না লিখে হাইলেভেল কোড লিখে ডেটা রিড অথবা রাইট করতে পারব। এটা ডেভেলপমেন্ট অনেক সহজ করে দেয়।
আমরা এর আগে দেখেছি যখন নতুন টেবিল বা নতুন ফিল্ড কোন টেবিলে যোগ করি, তখন কিছু এরর দেয়। এই এরর সমাধান করার সহজ পদ্ধতি হচ্ছে ডেটাবেজ ডিলেট করে নতুন করে তৈরি করা। কিন্তু প্রোডাকশন ডেটাবেজ তো এভাবে ডীলেট করা যাবে না। কারণ ইউজারের অনেক ডেটা থাকবে। তখন দরকার পড়ে ডেটাবেজ মাইগ্রেশনের। যা আমাদের ডেটাবেজে নতুন টেবিল এবং ফিল্ড তৈরির পর অটোমেটিক সব ফিল্ড ডেটাবেজে জেনারেট করে দিবে।
এক্সপ্রেসের জন্য সুন্দর একটা ORM এবং মাইগ্রেশন প্যাকেজ হচ্ছে Pisma। এক্সপ্রেসে মাল্টি টেন্যান্ট এপিআই তৈরি লেখা পর্যন্ত যতটুকু করেছি, সেখান থেকে শুরু করব। এর জন্য প্রথমে প্রিজমা ইন্সটল করব এবং ডেটাবেজ হিসেবে SQLite সেট করব। কেউ যদি একেবারে প্রথম থেকে শুরু করতে চান, নিচের দিকে তা নিয়ে লেখা রয়েছে।
npm install prisma --save-dev
npx prisma init --datasource-provider sqlite
যা আমাদের কিছু ফাইল তৈরি করে দিবে। যেমন prisma/schema.prisma, prisma.config.ts ইত্যাদি। এই ফাইলে আমাদের ডেটাবেজ URL যোগ করব। url = env("DATABASE_URL")
:
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
এই URL env ফাইল থেকে ব্যবহার করব, যেন একই URL একাধিক যায়গা থেকে এক্সেস করা যায়। এই জন্য dotenv প্যাকেজে যোগ করে নিবঃ
npm install dotenv
.env ফাইল ওপেন করে database URL টা যোগ করতে হবেঃ
DATABASE_URL="file:../notes.db"
এর আগে ডেটাবেজ স্কিমা লিখেছিলাম db.js এ। এবার তা লিখব prisma/schema.prisma ফাইলেঃ
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
// 🧍 User model
model User {
id Int @id @default(autoincrement())
username String @unique
password String
role String // admin | tenant | user
tenant Tenant? @relation(fields: [tenantId], references: [id])
tenantId Int?
notes Note[]
created_at DateTime @default(now())
}
// 🏢 Tenant model
model Tenant {
id Int @id @default(autoincrement())
name String @unique
users User[]
notes Note[]
created_at DateTime @default(now())
}
// 🗒️ Note model
model Note {
id Int @id @default(autoincrement())
title String
content String
user User @relation(fields: [userId], references: [id])
userId Int
tenant Tenant? @relation(fields: [tenantId], references: [id])
tenantId Int?
created_at DateTime @default(now())
}
ফলে db.js ফাইলটা থেকে স্কিম গুলো রিমুভ করে দিতে পারব। এবং সেখানে প্রিজমা ক্লায়েন্ট ইনিশিয়ালাইজ করে নিব:
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
export default prisma;
এবার ডেটাবেজ মাইগ্রেট করে নিবঃ
npx prisma migrate dev --name init
এবার routes/notes.js এ prismas ব্যবহার করে ডেটা কোয়েরি করতে পারবঃ
import express from "express";
import prisma from "../db.js";
import { authenticate } from "../middleware/auth.js";
const router = express.Router();
// ✅ All routes require authentication
router.use(authenticate);
/**
* 📘 Create Note
* - Regular users can create notes under their tenant
* - Tenants and Admins can also create (optional)
*/
router.post("/", async (req, res) => {
const { title, content } = req.body;
const { id, tenantId } = req.user;
try {
const note = await prisma.note.create({
data: {
title,
content,
userId: id,
tenantId: tenantId || null,
},
});
res.json(note);
} catch (err) {
console.error(err);
res.status(500).json({ error: "Failed to create note" });
}
});
/**
* 📖 Get All Notes
* - Admin: see all notes
* - Tenant: see all notes under their tenant
* - User: see only their own notes
*/
router.get("/", async (req, res) => {
const { role, id, tenantId } = req.user;
try {
let notes;
if (role === "admin") {
notes = await prisma.note.findMany({
orderBy: { created_at: 'desc' },
include: {
user: {
select: { id: true, username: true, role: true }
},
tenant: {
select: { id: true, name: true }
}
}
});
} else if (role === "tenant") {
notes = await prisma.note.findMany({
where: { tenantId: tenantId },
orderBy: { created_at: 'desc' },
include: {
user: {
select: { id: true, username: true, role: true }
}
}
});
} else {
notes = await prisma.note.findMany({
where: { userId: id },
orderBy: { created_at: 'desc' }
});
}
res.json(notes);
} catch (err) {
console.error(err);
res.status(500).json({ error: "Failed to fetch notes" });
}
});
/**
* 🔍 Get Single Note
* - Admin: can access any note
* - Tenant: can access notes within their tenant
* - User: only their own notes
*/
router.get("/:id", async (req, res) => {
const { role, id, tenantId } = req.user;
const noteId = parseInt(req.params.id);
try {
let whereClause = { id: noteId };
if (role === "tenant") {
whereClause.tenantId = tenantId;
} else if (role === "user") {
whereClause.userId = id;
}
// Admin can access any note, so no additional where clause needed
const note = await prisma.note.findFirst({
where: whereClause,
include: {
user: {
select: { id: true, username: true, role: true }
},
tenant: {
select: { id: true, name: true }
}
}
});
if (!note) return res.status(404).json({ error: "Note not found" });
res.json(note);
} catch (err) {
console.error(err);
res.status(500).json({ error: "Failed to fetch note" });
}
});
/**
* ✏️ Update Note
*/
router.put("/:id", async (req, res) => {
const { title, content } = req.body;
const { role, id, tenantId } = req.user;
const noteId = parseInt(req.params.id);
try {
let whereClause = { id: noteId };
if (role === "tenant") {
whereClause.tenantId = tenantId;
} else if (role === "user") {
whereClause.userId = id;
}
// Admin can update any note
const updatedNote = await prisma.note.updateMany({
where: whereClause,
data: {
title,
content,
},
});
if (updatedNote.count === 0) {
return res.status(404).json({ error: "Note not found or access denied" });
}
res.json({ message: "Note updated successfully" });
} catch (err) {
console.error(err);
res.status(500).json({ error: "Failed to update note" });
}
});
/**
* ❌ Delete Note
*/
router.delete("/:id", async (req, res) => {
const { role, id, tenantId } = req.user;
const noteId = parseInt(req.params.id);
try {
let whereClause = { id: noteId };
if (role === "tenant") {
whereClause.tenantId = tenantId;
} else if (role === "user") {
whereClause.userId = id;
}
// Admin can delete any note
const deletedNote = await prisma.note.deleteMany({
where: whereClause,
});
if (deletedNote.count === 0) {
return res.status(404).json({ error: "Note not found or access denied" });
}
res.json({ message: "Note deleted successfully" });
} catch (err) {
console.error(err);
res.status(500).json({ error: "Failed to delete note" });
}
});
export default router;
একই ভাবে routes/auth.js ও আপডেট করে নিবঃ
import express from "express";
import bcrypt from "bcryptjs";
import jwt from "jsonwebtoken";
import prisma from "../db.js";
import { authenticate, authorizeRoles } from "../middleware/auth.js";
const router = express.Router();
const JWT_SECRET = "s9d8f7s9df7s9dadsfdf7s9df7s9df"; // use env in prod
// 🧱 Register Tenant (only admin can do this)
router.post("/register-tenant", authenticate, authorizeRoles("admin"), async (req, res) => {
const { tenantName, username, password } = req.body;
const hashedPassword = await bcrypt.hash(password, 10);
try {
// Create tenant and tenant user in a transaction
const result = await prisma.$transaction(async (tx) => {
// Create the tenant
const tenant = await tx.tenant.create({
data: {
name: tenantName,
},
});
// Create the tenant admin user
const user = await tx.user.create({
data: {
username,
password: hashedPassword,
role: "tenant",
tenantId: tenant.id,
},
});
return { tenant, user };
});
res.json({
message: "Tenant created",
tenantId: result.tenant.id,
userId: result.user.id
});
} catch (err) {
console.error(err);
res.status(400).json({ error: "Tenant or username already exists" });
}
});
// 👤 Register Regular User (only tenant admin can do this)
router.post("/register", authenticate, authorizeRoles("tenant"), async (req, res) => {
const { username, password } = req.body;
const hashedPassword = await bcrypt.hash(password, 10);
try {
const user = await prisma.user.create({
data: {
username,
password: hashedPassword,
role: "user",
tenantId: req.user.tenantId,
},
});
res.json({ message: "User created", id: user.id });
} catch (err) {
console.error(err);
res.status(400).json({ error: "Username already exists" });
}
});
// 🔑 Login (works for all roles)
router.post("/login", async (req, res) => {
const { username, password } = req.body;
try {
const user = await prisma.user.findUnique({
where: { username },
include: {
tenant: {
select: { id: true, name: true }
}
}
});
if (!user) {
return res.status(400).json({ error: "Invalid credentials" });
}
const valid = await bcrypt.compare(password, user.password);
if (!valid) {
return res.status(400).json({ error: "Invalid credentials" });
}
const token = jwt.sign(
{
id: user.id,
role: user.role,
tenantId: user.tenantId,
username: user.username
},
JWT_SECRET,
{ expiresIn: "1h" }
);
res.json({
token,
role: user.role,
user: {
id: user.id,
username: user.username,
role: user.role,
tenant: user.tenant
}
});
} catch (err) {
console.error(err);
res.status(500).json({ error: "Login failed" });
}
});
export default router;
এবার আমরা ইউজার রেজিস্ট্রেশন থেকে শুরু করে সব কিছুই সুন্দর মত করতে পারব। এর আগে আমরা দেখেছি সুপার এডমিন কিভাবে SQL কমান্ডে তৈরি করেছি। এবার চাইলে ডেটাবেজ সিড ব্যবহার করে সুপার এডমিন তৈরি করে নিতে পারব। তার জন্য prisma ফোল্ডারে seed.js নামে একটা ফাইল তৈরি করব। এরপর লিখবঃ
import { PrismaClient } from "@prisma/client";
import bcrypt from "bcryptjs";
const prisma = new PrismaClient();
async function main() {
// Check if superadmin already exists
const existing = await prisma.user.findUnique({
where: { username: "superadmin" },
});
if (!existing) {
const hashedPassword = await bcrypt.hash("password", 10);
await prisma.user.create({
data: {
username: "superadmin",
password: hashedPassword,
role: "admin", // superadmin role
},
});
console.log("✅ Superadmin created");
} else {
console.log("ℹ️ Superadmin already exists");
}
}
main()
.catch((e) => console.error(e))
.finally(async () => {
await prisma.$disconnect();
});
এরপর package.json এর এর স্ক্রিপ্টে “seed”: “node prisma/seed.js” যোগ করব।
"scripts": {
"dev": "nodemon index.js",
"seed": "node prisma/seed.js"
},
এখন যদি npm run seed
কমান্ড রান করি, তাহলে ডেটাবেজে সুপার এডমিন তৈরি হবে। এবার এই এডমিন ক্রেডেনশিয়াল ব্যবহার করে আমরা লগিন করতে পারব, নতুন টেন্যান্ট যোগ করতে পারব ইত্যাদি ইত্যাদি।
মাইগ্রেশন ওয়ার্কফ্লো
আমাদের নোট মডেল এমনঃ
// 🗒️ Note model
model Note {
id Int @id @default(autoincrement())
title String
content String
user User @relation(fields: [userId], references: [id])
userId Int
tenant Tenant? @relation(fields: [tenantId], references: [id])
tenantId Int?
created_at DateTime @default(now())
}
আমরা চাচ্ছি নোট মডেলে নতুন একটা ফিল্ড যোগ করতে, যেমন tags। তার জন্য schema.prisma এডিট করে tags ফিল্ড যোগ করব।
// 🗒️ Note model
model Note {
id Int @id @default(autoincrement())
title String
content String
tags String?
user User @relation(fields: [userId], references: [id])
userId Int
tenant Tenant? @relation(fields: [tenantId], references: [id])
tenantId Int?
created_at DateTime @default(now())
}
এরপর নিচের কমান্ড লিখবঃ
npx prisma migrate dev --name add_tags_field
এই কমান্ড বেশ কয়েকটা কাজ করবে। যেমন একটা মাইগ্রেশন ফাইল তৈরি করবে, ডেটাবেজ স্কিমার সাথে বর্তমান ডেটাবেজ কম্পেয়ার করবে, মাইগ্রেশন এপ্লাই করবে।
এখন যদি ডেটাবেজ দেখি, দেখব Notes টেবিলে tags নামে নতুন ফিল্ড তৈরি হয়েছে।
যখন আমরা মাইগ্রেট কমান্ড রান করি, তখন অটোমেটিক্যালি প্রিজমা ক্লায়েন্ট আপডেট করে। এরপরও চাইলে দরকার অনুযায়ী নিচের কমান্ড রান করতে পারিঃ
npx prisma generate
ট্যাগ ফিল্ড যোগ করার পর আমরা এবার নোট তৈরির সময় এই ট্যাগ ফিল্ড পাস করতে পারব। তখন তা ডেটাবেজে সেভ হবে। create মেথড এভাবে আপডেট করবঃ
/**
* 📘 Create Note
* - Regular users can create notes under their tenant
* - Tenants and Admins can also create (optional)
*/
router.post("/", async (req, res) => {
const { title, content, tags } = req.body;
const { id, tenantId } = req.user;
try {
const note = await prisma.note.create({
data: {
title,
content,
userId: id,
tags,
tenantId: tenantId || null,
},
});
res.json(note);
} catch (err) {
console.error(err);
res.status(500).json({ error: "Failed to create note" });
}
});
এখানে সুধু tags যোগ করা হয়েছে। আর কোন কোড পরিবর্তন করতে হবে না। এরপর এভাবে ট্যাগ পাস করতে পারিঃ
{
"title": "Note with tags",
"content": "this is a note with tags!",
"tags": "prisma,express"
}
এবার যদি আমরা GET মেথডে /notes এ রিকোয়েস্ট করি, দেখতে পাবো ট্যাগ সহ নোট রিটার্ণ করছে।
আপডেট মেথডেও ট্যাগ যোগ করে দিতে পারেন।
এই তো! মাইগ্রেশন এবং ORM ডেভেলপমেন্ট অনেক সহজ করে।
এক্সপ্রেস প্রজেক্টের শুরু থেকেই Prisma ব্যবহার
এর আগে আমর একটা এক্সিস্টিং প্রজেক্টে প্রিজমা ব্যবহার করেছি। এবার আমরা একটা প্রজেক্ট তৈরি করার শুরু থেকেই প্রিজমা ব্যবহার করব। তার জন্য যে ফোল্ডারে প্রজেক্ট তৈরি করব, সেখানে কমান্ডলাইন বা টার্মিনালে গিয়ে লিখবঃ
npm init -y
npm install express dotenv @prisma/client
npm install prisma --save-dev
এরপর প্রিজমা ইনিশিয়ালাইজ করে নিতে হবেঃ
npx prisma init --datasource-provider sqlite
এখানে আমরা ডেটাবেজ হিসেবে SQLite ব্যবহার করছি। আপনি চাইলে যে কোন ডেটাবেজই ব্যবহার করতে পারবেন। উপরের কমান্ড .env, prisma/schema.prisma এবং prisma.config.ts যোগ করে দিবে।
prisma.config.ts এ dotenv ইম্পোর্ট করা না থাকলে তা ইম্পোর্ট করে নিতে হবেঃ
import "dotenv/config";
import { defineConfig, env } from "prisma/config";
export default defineConfig({
schema: "prisma/schema.prisma",
migrations: {
path: "prisma/migrations",
},
engine: "classic",
datasource: {
url: env("DATABASE_URL"),
},
});
schema.prisma ফাইলে ডেটাবেজ স্কিমা, ডেটাবেজ লোকেশন ইত্যাদি সেট করতে হবে। আমরা সিম্পল নোট অ্যাপ তৈরি করব। তাই নিচের মত করে লিখবঃ
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
model Note {
id Int @id @default(autoincrement())
title String
content String
tags String? // optional field
created_at DateTime @default(now())
}
.env ফোল্ডার নিচের মত একটা ডেটাবেজ URL এড করা থাকবেঃ
DATABASE_URL="file:./dev.db"
আমরা চাইলে এটা পরিবর্তন করতে পারব। আপাতত যেভাবে আছে সেভাবেই রাখব।
এরপর নিচের কমান্ড রান করবঃ
npx prisma migrate dev --name init
যা আমাদের ডেটাবেজ তৈরি করে দিবে, প্রিজমা ক্লায়েন্ট তৈরি করবে এবং মাইগ্রেশন ফাইল তৈরি করবে।
এবার db.js নামে একটা ফাইল তৈরি করব, যেখানে প্রিজমা ক্লায়েন্ট ইনিশিয়ালাইজ করে নিবঃ
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
export default prisma;
এবং ফাইনালি index.js ফাইলে আমাদের রাউট কোড লিখবঃ
import express from "express";
import prisma from "./db.js";
const app = express();
app.use(express.json());
const PORT = 3000;
// 📝 Create Note
app.post("/notes", async (req, res) => {
const { title, content, tags } = req.body;
const note = await prisma.note.create({
data: { title, content, tags },
});
res.json(note);
});
// 📋 Get All Notes
app.get("/notes", async (req, res) => {
const notes = await prisma.note.findMany({ orderBy: { created_at: "desc" } });
res.json(notes);
});
// 🔍 Get Single Note
app.get("/notes/:id", async (req, res) => {
const note = await prisma.note.findUnique({ where: { id: Number(req.params.id) } });
if (!note) return res.status(404).json({ error: "Note not found" });
res.json(note);
});
// ✏️ Update Note
app.put("/notes/:id", async (req, res) => {
const { title, content, tags } = req.body;
try {
const note = await prisma.note.update({
where: { id: Number(req.params.id) },
data: { title, content, tags },
});
res.json(note);
} catch {
res.status(404).json({ error: "Note not found" });
}
});
// ❌ Delete Note
app.delete("/notes/:id", async (req, res) => {
try {
await prisma.note.delete({ where: { id: Number(req.params.id) } });
res.json({ message: "Note deleted" });
} catch {
res.status(404).json({ error: "Note not found" });
}
});
// Start server
app.listen(PORT, () => console.log(`Server running on http://localhost:${PORT}`));
এবার পোস্টম্যানে গিয়ে পোস্ট রিকোয়েস্ট করে নোট যোগ করতে পারবঃ
POST http://localhost:3000/notes
Content-Type: application/json
{
"title": "First Note",
"content": "This is a simple note",
"tags": "learning,prisma"
}
নোট গুলো দেখতে /notes রাউটে গেট রিকোয়েস্ট করলে নোট গুলো দেখতে পাবো।
এছাড়া আমরা চাইলে প্রিজমা স্টুডিও ব্যবহার করেও ডেটাবেজে কি কি ডেটা রয়েছে তা দেখা সহ নতুন ডেটা যোগ করতে পারব। তার জন্য লিখবঃ
npx prisma studio
যা ব্রাউজারে প্রিজমা স্টুডিও ওপেন করবে।
এই তো। এবার চাইলে প্রিজমা স্কিমা ফাইলে নতুন ফিল্ড যোগ করে মাইগ্রেট করে নিতে পারেন। উপরে মাইগ্রেশন ওয়ার্কফ্লো সম্পর্কে লেখা রয়েছে, তা দেখে নিতে পারেন।