// Real-World Backend Engineering
তুমি কি জানো,
বেশিরভাগ Backend
কেন ভেঙে পড়ে?
Raw SQL লেখা, copy-paste করা query, কোনো structure নেই — এই ভুলগুলো ছোট project এ ধরা পড়ে না।
কিন্তু যখন লাখ লাখ user আসে, তখন সব ভেঙে পড়ে।
আজকে দেখবো — Prisma + PostgreSQL দিয়ে কীভাবে এমন একটা system বানানো যায় যেটা reusable, maintainable, এবং scalable —
production-ready কোড দিয়ে।
PostgreSQL
Prisma ORM
Design Patterns
Reusable Architecture
02
// The Problem
যে ভুলটা সবাই করে
Junior থেকে mid-level — সবাই এই trap এ পড়ে।
❌ সমস্যা যেভাবে লেখে
// Route handler এ সরাসরি DB call
app.get('/courses',
async (req, res) => {
const page = req.query.page || 1;
const limit = req.query.limit || 10;
// কোনো validation নেই
// কোনো error handling নেই
// duplicate code হবে সব জায়গায়
const data = await
prisma.course.findMany({
skip: (page - 1) * limit,
take: Number(limit),
// search? filter? sort?
// copy করতে হবে আবার...
});
res.json(data);
});
✅ Production-ready পদ্ধতি
// Clean service layer separation
const { skip, take, orderBy, where }
= buildQueryOptions(
query,
searchableFields,
sortableFields,
filterableFields
);
const [data, total] = await Promise.all([
prisma.course.findMany({ where, skip,
take, orderBy }),
prisma.course.count({ where }),
]);
// ✅ Reusable everywhere
// ✅ Search + Sort + Filter + Pagination
এই পার্থক্যটা বোঝা দরকার কারণ...
১০০টা endpoint এ আলাদা আলাদা pagination logic = ১০০টা জায়গায় bug। একটা
reusable helper = একবার fix, সব ঠিক।
03
// Architecture
Module-Based Architecture
প্রতিটা feature তার নিজের layer এ থাকে — সব কিছু
predictable।
src/app/modules/
├── student/
│ ├── courseStudent.routes.js
│ ├── courseStudent.controller.js
│ ├── courseStudent.services.js
│ ├── courseStudent.constants.js
│ └── courseStudent.cache.js
├── admin/
│ ├── class.routes.js
│ ├── class.controller.js
│ └── class.services.js
src/helper/
├── buildQueryOptions.js ←
shared
└── activityLog.js ←
shared
01
Route Layer
URL mapping, middleware attach, validation
routes.js
02
Controller Layer
Request parse, response send — কোনো business logic নেই
controller.js
03
Service Layer
সব business logic এখানে — Prisma call করে
services.js
04
Constants Layer
searchableFields, filterableFields, sortableFields
constants.js
05
Cache Layer
Redis-backed cache, DB load কমায়
cache.js
04
// The Magic Helper
buildQueryOptions()
একটাই function — pagination, search, sort, filter সব handle
করে। লিখতে হয় একবার, ব্যবহার হয় সব জায়গায়।
export const buildQueryOptions = (
query = {},
searchableFields = [], // partial match
sortableFields = [], // allowed sorts
exactMatchFields = [], // filter=value
) => {
const page = parseInt(query?.page) || 1;
const limit = Math.min(
parseInt(query?.limit) || 10, 1000
);
const sortBy = query?.sortBy || 'createdAt';
const sortOrder = query?.sortOrder || 'desc';
const skip = (page - 1) * limit;
const orderBy = sortableFields.includes(sortBy)
? { [sortBy]: sortOrder === 'asc' ? 'asc' : 'desc' }
: {};
// search বা exact filter — দুটো একসাথে না
const where = buildWhere(
query, searchableFields, exactMatchFields
);
return { skip, take: limit, orderBy, where };
};
Query
Parameters → Prisma Options
?searchTerm=math
→
OR [contains: "math"]
?filter=active
→
OR [equals: "active"]
?page=2&limit=20
→
skip: 20, take: 20
?sortBy=title&sortOrder=asc
→
orderBy: {title: 'asc'}
Security:
limit max 1000 — DDoS protection
built-in
Nested Field Support
"course.title"
→ { course: { title: { contains: ... } } }
Prisma
এর nested relation query automatic।
05
// Constants Pattern
Constants দিয়ে Behavior Control
কোন field এ search হবে, কোনটায় sort হবে — এটা code এ
hardcode না করে, constants file এ declare করো।
constants.js
export const searchableFields = [
'course.title',
'course.productName',
'student.fullName', ← nested!
];
export const sortableFields = [
'createdAt',
'updatedAt',
'enrolledAt',
];
export const filterableFields = [
'status',
'paymentStatus',
];
export const selectFields = {
id: true,
studentId: true,
course: {
select: { title: true, courseImage: true }
}
};
কেন এটা ভালো?
Service file খুলতে হয় না। শুধু constants.js বদলালেই পুরো module এর
behavior বদলে যায়। নতুন developer ও instantly বুঝতে পারে।
🔒
SQL Injection Protection
sortableFields.includes(sortBy)
check করে — user যা পাঠাক, শুধু allow-listed field এ sort হবে।
⚡
Select Optimization
Prisma এর select দিয়ে শুধু
দরকারি column fetch — unnecessary data transfer নেই।
🔄
Module Independence
প্রতিটা module এর আলাদা constants — student module এর search field admin
module কে affect করে না।
06
// Validation Pattern
Zod + Middleware = Clean Validation
Database এ ভুল data ঢোকার আগেই block করো — Zod schema +
reusable middleware দিয়ে।
// validation.middleware.js
const validationRequest = (schema) => {
return async (req, res, next) => {
try {
const parsed = await schema.parseAsync({
body: req.body,
query: req.query,
params: req.params,
});
req.body = parsed.body;
next();
} catch (err) {
next(err); // Zod error → global handler
}
};
};
// routes.js এ use:
router.post('/enroll',
validationRequest(enrollmentSchema),
controller.enroll
);
// Zod schema
const enrollmentSchema = z.object({
body: z.object({
courseId: z.string().uuid(),
paymentMethod: z.enum([
'BKASH', 'NAGAD', 'BANK'
]),
})
});
Request
Flow
Client Request
Zod Validate
Service Layer
Prisma → DB
✓
Type check, format check — controller
এ একটা line ও লাগে না
✓
Error message automatic — Zod বলে
দেয় ঠিক কোন field ভুল
✓
Schema reuse করা যায় — create ও
update এ same schema extend
✓
TypeScript-like type safety —
JavaScript এও
07
// Error Handling Pattern
Centralized Error Handling
প্রতিটা route এ try/catch লিখলে code mess হয়। একটা
centralized system দিয়ে সব errors handle করো।
// AppErrors.js — Custom Error Class
class AppErrors extends
Error {
constructor(statusCode, message) {
super(message);
this.statusCode = statusCode;
this.isOperational = true;
}
}
// Service এ throw করো:
if (!user) throw new AppErrors(
StatusCodes.NOT_FOUND,
'User not found'
);
if (!hasAccess) throw new AppErrors(
StatusCodes.FORBIDDEN,
'Course access denied'
);
// Global error handler (app.js):
app.use((err, req, res, next) => {
if (err.isOperational) {
res.status(err.statusCode).json({
success: false,
message: err.message
});
} else {
// Unexpected error — log & 500
console.error(err);
res.status(500).json({
... });
}
});
0
Route এ try/catch দরকার
1
Global handler সব handle করে
isOperational Flag
AppErrors দিয়ে throw হলে isOperational = true
— এটা expected error (user না পাওয়া, permission নেই)। Unexpected crash হলে false — আলাদাভাবে
log করো।
NOT_FOUND (404)
user/course/data
নেই
FORBIDDEN (403)
access
নেই
UNAUTHORIZED (401)
token
নেই/expired
CONFLICT (409)
duplicate
data
08
// Cache Layer
Redis Cache on Top of Prisma
লাখ user এর জন্য প্রতিটা request এ DB hit করলে system মরবে।
Redis cache দিয়ে DB load ৯০%+ কমাও।
// getOrLoadStrictCache pattern
const getCachedAuthUser = async (userId) => {
const cacheKey = `auth:user:${userId}`;
return await getOrLoadStrictCache(
cacheKey,
async () => {
// শুধু cache miss হলে DB call
return await prisma.user.findUnique({
where: { id: userId },
select: { id: true, role: true,
status: true }
});
},
{ ttl: 120 } // 2 minutes TTL
);
};
// Enrollment change হলে cache invalidate:
await invalidateCourseStudentAccess(
studentId, courseId
);
Cache
Keys Architecture
🔑
auth:user:{userId}
2min TTL
🔑
auth:session:{userId}
2min TTL
🔑
course:access:{studentId}:{courseId}
auto clear
🔑
course:context:{entityId}
2min TTL
Cache Invalidation Strategy
Data বদলালে cache clear করতে হয়। Enrollment হলে → invalidateCourseStudentAccess()।
Many students এ → invalidateCourseStudentAccessMany()।
09
// End-to-End Flow
একটা API Request এর পুরো journey
GET /my-courses request পাঠালে কী হয় — step by step।
01
Route →
validationRequest(schema) → authorize(['student'])
Zod দিয়ে query validate, তারপর JWT verify + role check
~2ms
02
Redis Cache Check —
getCachedAuthUser(userId)
User data cache এ আছে? → directly use। নেই? → Prisma call
~1ms hit
03
Controller →
service.getMyCourses(query, payload)
Request parsed, payload (userId, role) extracted থেকে service call
~0ms
04
buildQueryOptions(query,
fields…) → Prisma options
search, sort, filter, pagination — সব resolve হয়ে যায় এখানে
~0ms
05
Promise.all([findMany, count]) →
PostgreSQL
Data + total count — দুটো query parallel এ চলে, time অর্ধেক
~15ms
06
Response: { data, meta: {
page, limit, total } }
Consistent response format — সব endpoint এ same structure
✓ Done
10
// Summary
এই Patterns গুলো দিয়ে কী পেলাম?
একটা production system এ এই design decisions এর impact।
♻️
Reusability
buildQueryOptions একবার লেখা, ৫০+ endpoint এ use। এক জায়গায় fix = সব
জায়গায় fix।
🧩
Maintainability
Module-based structure — নতুন developer আসলে instantly বুঝতে পারে কোথায় কী
আছে।
🚀
Scalability
Redis cache + Prisma optimize query — লাখ user এও DB overwhelm হয় না।
🔒
Security
Zod validation, allowlist sort fields, AppErrors — সব layer এ protection।
🐛
Debuggability
Centralized error handling + activity log — production issue instantly track
করা যায়।
⚡
Performance
Promise.all parallel queries, select optimization, limit max 1000 — সব কিছু
intentional।
মূল কথা
Good architecture মানে বেশি code লেখা না — মানে কম code দিয়ে বেশি করা। এই patterns গুলো practice করো, তোমার
project ও production-ready হবে।