Generating & Instantiating Prisma Client
Every backend application ultimately boils down to one thing: data. Creating it, reading it, updating it, deleting it. In this blog, we’ll master Prisma Client’s CRUD operations and wire them into a fully functional REST API. By the end, you’ll have a Users API that you’d actually be comfortable shipping.
Generating & Instantiating Prisma Client
Setup in 60 Seconds
npx prisma generate
The Singleton Pattern
In Express apps, each import of `PrismaClient` creates a new database connection pool. During development with hot-reload (nodemon), this causes connection exhaustion. The solution: a singleton.
// src/lib/prisma.ts
import { PrismaClient } from '@prisma/client';
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
export const prisma =
globalForPrisma.prisma ??
new PrismaClient({
log: process.env.NODE_ENV === 'development' ? ['query', 'warn', 'error'] : ['error'],
});
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma;
}
Pro Tip
CREATE Operations
`create` — Single Record
// src/lib/prisma.ts
import { PrismaClient } from '@prisma/client';
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
export const prisma =
globalForPrisma.prisma ??
new PrismaClient({
log: process.env.NODE_ENV === 'development' ? ['query', 'warn', 'error'] : ['error'],
});
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma;
}
`createMany` — Bulk Insert
const result = await prisma.user.createMany({
data: [
{ email: '[email protected]', name: 'Alice' },
{ email: '[email protected]', name: 'Bob' },
],
skipDuplicates: true, // MySQL: skips rows that violate unique constraints
});
// result.count = number of inserted records
READ Operations
`findUnique` — Exact Match (by `@id` or `@unique`)
const user = await prisma.user.findUnique({
where: { email: '[email protected]' },
});
// Returns: User | null
`findFirst` — First Match (any field)
const user = await prisma.user.findFirst({
where: { name: { contains: 'John' } },
orderBy: { createdAt: 'desc' },
});
`findMany` — Multiple Records
const users = await prisma.user.findMany({
where: { active: true },
take: 10,
skip: 0,
orderBy: { createdAt: 'desc' },
});
Performance
`findUnique` is faster than `findFirst` — it generates an indexed lookup. Always prefer `findUnique` when querying by `@id` or `@unique` fields. (prisma.io/docs/reference/api-reference/prisma-client-reference#findunique)
UPDATE Operations
`update` — Single Record
const user = await prisma.user.update({
where: { id: 1 },
data: { name: 'Jane Doe' },
});
`updateMany` — Bulk Update
const result = await prisma.user.updateMany({
where: { active: false },
data: { deletedAt: new Date() },
});
// result.count = number of updated records
`upsert` — Create or Update
One of Prisma’s most powerful methods — insert if not exists, update if it does:
const user = await prisma.user.upsert({
where: { email: '[email protected]' },
update: { name: 'John Updated' },
create: { email: '[email protected]', name: 'John New' },
});
DELETE Operations
// Delete single
await prisma.user.delete({ where: { id: 1 } });
// Delete many
await prisma.user.deleteMany({
where: { createdAt: { lt: new Date('2020-01-01') } }
});
Warning
Selecting Specific Fields
const user = await prisma.user.findUnique({
where: { id: 1 },
select: {
id: true,
email: true,
name: true,
// password: omitted — never returned to clients
},
});
Excluding Sensitive Fields (Password, Tokens)
const user = await prisma.user.findUnique({
where: { id: 1 },
select: {
id: true,
email: true,
name: true,
// password: omitted — never returned to clients
},
});
IndexedDB, Cookies, or Anywhere
Handling Complex Data Types
JSON.stringify can’t handle Maps, Sets, Dates, or RegExp. Solution: custom serialization. The Zustand GitHub documentation shows using superjson or similar libraries (serialize-javascript, devalue) for complex types.
import superjson from 'superjson'
import { PersistStorage } from 'zustand/middleware'
interface ComplexState {
userMap: Map<string, User>
tags: Set<string>
createdAt: Date
pattern: RegExp
}
const storage: PersistStorage<ComplexState> = {
getItem: (name) => {
const str = localStorage.getItem(name)
if (!str) return null
return superjson.parse(str)
},
setItem: (name, value) => {
localStorage.setItem(name, superjson.stringify(value))
},
removeItem: (name) => localStorage.removeItem(name),
}
const useComplexStore = create<ComplexState>()(
persist(
(set) => ({
userMap: new Map(),
tags: new Set(),
createdAt: new Date(),
pattern: new RegExp(''),
// ... actions
}),
{
name: 'complex-storage',
storage,
}
)
)
Migration
interface StateV1 {
count: number
}
interface StateV2 {
count: number
multiplier: number // New field
}
const useStore = create<StateV2>()(
persist(
(set) => ({
count: 0,
multiplier: 1,
increment: () => set((state) => ({
count: state.count + state.multiplier
})),
}),
{
name: 'counter-storage',
version: 2, // Increment when schema changes
migrate: (persistedState: any, version: number) => {
if (version === 1) {
// Migrate from v1 to v2
return {
...persistedState,
multiplier: 1, // Add missing field
}
}
return persistedState
},
}
)
)
Combining Middleware
Middleware order affects behavior. General rule: devtools → persist → immer
import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'
import { immer } from 'zustand/middleware/immer'
const useStore = create<State>()(
devtools( // 1. Outermost: debugging
persist( // 2. Middle: persistence
immer( // 3. Innermost: mutations
(set) => ({
user: { profile: { name: '' } },
updateName: (name) => set((state) => {
state.user.profile.name = name // Immer magic
}),
})
),
{ name: 'user-storage' }
),
{ name: 'UserStore' }
)
)
Why this order?
- DevTools should see all changes (outermost)
- Persist should work with final state after immer processes it
- Immer transforms mutations to immutable updates (innermost)
Common Pitfalls
| Issue | Problem | Solution |
|---|---|---|
| Persisting Functions | Don't try to persist actions. They can't be serialized. | Use partialize to exclude them. |
| Large State Objects | localStorage has ~5-10MB limit. | Use IndexedDB for large data, or selectively persist. |
| Sensitive Data | Never persist passwords or tokens in localStorage (XSS vulnerable). | Use httpOnly cookies for sensitive auth data. |
Data that doesn't persist is data that doesn't matter.
Thank You for Spending Your Valuable Time
I truly appreciate you taking the time to read blog. Your valuable time means a lot to me, and I hope you found the content insightful and engaging!
Frequently Asked Questions
Not directly with persist middleware. You'd need custom logic. Consider why you need both—usually one suffices.
Access `useStore.persist.clearStorage()`. Example: `useAuthStore.persist.clearStorage()` on logout.
Yes! Use AsyncStorage instead of localStorage: `storage: createJSONStorage(() => AsyncStorage)`.
The persist middleware will fail silently. Monitor storage usage and implement error handling via `onRehydrateStorage` callback.
Yes, via `useStore.persist.rehydrate()`. Useful for testing or manual sync scenarios.
Comments are closed