Once CRUD works, make it fast. Two issues dominate: the N+1 query problem and missing caches.
1const tasks = await prisma.task.findMany(); // 1 query
2for (const t of tasks) {
3 t.owner = await prisma.user.findUnique({ where: { id: t.ownerId } }); // N queries!
4}Twenty tasks → twenty-one queries. This is the N+1 problem, the #1 cause of slow list endpoints.
1const tasks = await prisma.task.findMany({
2 include: { owner: { select: { id: true, name: true } } },
3});One query (or a small constant number). Always load the relations you'll render up front.
Reads usually outnumber writes 100:1. Cache them.
| Layer | Tool | Use for |
|---|---|---|
| HTTP | Cache-Control, CDN | Public, rarely-changing GETs |
| App | Redis / in-memory | Hot lookups, computed lists |
| Query | ORM / DB cache | Repeated identical queries |
1const cached = await redis.get(key);
2if (cached) return JSON.parse(cached);
3const data = await prisma.task.findMany({ where });
4await redis.set(key, JSON.stringify(data), "EX", 60); // 60s TTL
5return data;A cache is only safe if writes clear it. After any Create/Update/Delete, bust the relevant keys:
1await prisma.task.update({ where: { id }, data });
2await redis.del(`tasks:${ownerId}`); // the read cache is now stale — drop it"There are only two hard things in computer science: cache invalidation and naming things." Cache the obvious wins, invalidate on every write, and measure before optimizing further.
Add query logging and timing. Optimize the endpoints that are actually slow under real traffic — not the ones you guess are slow.