- Published on
The Hidden Danger of Importing Prisma Enums in Client-Side Code
- Authors
Building a modern web application with Prisma ORM and meta-frameworks like Next.js or SvelteKit can lead to a frustrating build error that's hard to debug. Here's the problem I encountered, why it happens, and the solution that finally worked.
The Problem
Everything works perfectly in development, but when you try to build for production, you encounter a cryptic error:
ERROR Build Error: Could not resolve "../.prisma/client/index-browser"
from "../.prisma/client/index-browser?commonjs-external"
The build fails, and you're left wondering what went wrong. The error message doesn't immediately point to the root cause, making it frustrating to debug.
Understanding the Error
This error occurs when your bundler (Vite, Webpack, Rollup, etc.) attempts to resolve Prisma's client code for browser environments but encounters conflicts between server-side and client-side module resolution.
The key issue is that Prisma Client is designed to run on the server, not in the browser. When you import Prisma enums directly in client-side code, your bundler tries to include server-only dependencies in the client bundle, leading to resolution conflicts.
Root Cause Analysis
Why Does This Happen?
- Prisma Client Architecture: Prisma generates a client that includes database drivers and server-specific dependencies
- Bundler Confusion: When you import from
@prisma/clientin frontend code, the bundler attempts to resolve these server dependencies for the browser - Module Resolution Conflicts: The browser version of Prisma client (
index-browser) lacks the full server functionality, causing resolution failures
The Problematic Pattern
// ❌ WRONG: This breaks production builds
import { UserRole, PostStatus } from '@prisma/client'
// In a Vue component, React component, or any client-side code
const userRoles = Object.values(UserRole)
const availableStatuses = [PostStatus.DRAFT, PostStatus.PUBLISHED]
This seems innocent and works in development because development servers often have more permissive module resolution. However, production builds are stricter and expose this architectural mismatch.
The Solution: Constants Pattern
The solution I found is to create typed constant objects that mirror your Prisma enums without importing the actual Prisma client.
Step 1: Create Typed Constants
// constants/enums.ts
import type { UserRole, PostStatus } from '@prisma/client' // ✅ Type-only import
// Create runtime constants that satisfy the enum types
export const USER_ROLES = {
ADMIN: 'ADMIN',
MODERATOR: 'MODERATOR',
USER: 'USER',
} as const satisfies { [K in UserRole]: K }
export const POST_STATUSES = {
DRAFT: 'DRAFT',
PUBLISHED: 'PUBLISHED',
ARCHIVED: 'ARCHIVED',
} as const satisfies { [K in PostStatus]: K }
// Type helpers for better DX
export type UserRoleType = keyof typeof USER_ROLES
export type PostStatusType = keyof typeof POST_STATUSES
Step 2: Use Constants in Client Code
// ✅ CORRECT: Use constants in client-side code
import { USER_ROLES, POST_STATUSES } from '~/constants/enums'
import type { UserRole, PostStatus } from '@prisma/client' // Type-only import OK
// Now you can safely use these in components
const availableRoles = Object.values(USER_ROLES)
const defaultStatus = POST_STATUSES.DRAFT
// TypeScript still provides full type safety
function updateUserRole(role: UserRole) {
// role is properly typed as 'ADMIN' | 'MODERATOR' | 'USER'
console.log(`Updating role to: ${role}`)
}
Step 3: Server-Side Usage Remains Unchanged
// server/api/users.ts - Server-side code can import directly
import { UserRole, PostStatus } from '@prisma/client' // ✅ OK on server
import { prisma } from '~/lib/prisma'
export async function createUser(data: { role: UserRole }) {
return prisma.user.create({
data: {
role: UserRole.USER, // Direct enum usage OK on server
// ...other fields
},
})
}
Advanced Patterns
Generic Enum Helper
For projects with many enums, create a generic helper:
// utils/enum-helpers.ts
export function createEnumConstants<T extends Record<string, string>>(
enumType: T
): { [K in keyof T]: T[K] } {
const constants = {} as { [K in keyof T]: T[K] }
for (const key in enumType) {
constants[key] = enumType[key]
}
return constants
}
// Usage with type assertion (use carefully)
import type { UserRole } from '@prisma/client'
export const USER_ROLES = createEnumConstants({
ADMIN: 'ADMIN',
MODERATOR: 'MODERATOR',
USER: 'USER',
} as const satisfies { [K in UserRole]: K })
Validation Helpers
Create validation functions using your constants:
// utils/validators.ts
import { USER_ROLES, POST_STATUSES } from '~/constants/enums'
import type { UserRole, PostStatus } from '@prisma/client'
export function isValidUserRole(value: string): value is UserRole {
return Object.values(USER_ROLES).includes(value as UserRole)
}
export function isValidPostStatus(value: string): value is PostStatus {
return Object.values(POST_STATUSES).includes(value as PostStatus)
}
Best Practices
1. Establish Clear Import Rules
Create linting rules or team conventions:
// ✅ ALWAYS: Type-only imports from Prisma in client code
import type { UserRole } from '@prisma/client'
// ✅ ALWAYS: Runtime constants for client-side usage
import { USER_ROLES } from '~/constants/enums'
// ❌ NEVER: Direct enum imports in client code
import { UserRole } from '@prisma/client'
2. Maintain Consistency
Keep your constants in sync with your Prisma schema:
// prisma/schema.prisma
enum UserRole {
ADMIN
MODERATOR
USER
}
// constants/enums.ts - Must match exactly
export const USER_ROLES = {
ADMIN: 'ADMIN',
MODERATOR: 'MODERATOR',
USER: 'USER',
} as const satisfies { [K in UserRole]: K }
3. Use TypeScript for Safety
The satisfies operator ensures your constants match the Prisma enum exactly:
// This will cause a TypeScript error if USER_ROLES doesn't match UserRole
export const USER_ROLES = {
ADMIN: 'ADMIN',
MODERATOR: 'MODERATOR',
// USER: 'USER', // Missing - TypeScript error!
} as const satisfies { [K in UserRole]: K }
4. Organize Constants Logically
constants/
├── user.constants.ts # User-related enums
├── post.constants.ts # Post-related enums
├── order.constants.ts # Order-related enums
└── index.ts # Re-exports
Common Pitfalls to Avoid
1. Importing in Shared Utilities
// ❌ WRONG: Utility used by both client and server
import { UserRole } from '@prisma/client'
export function formatUserRole(role: UserRole) {
return role.toLowerCase()
}
// ✅ CORRECT: Use constants in shared utilities
import { USER_ROLES } from '~/constants/enums'
import type { UserRole } from '@prisma/client'
export function formatUserRole(role: UserRole) {
return role.toLowerCase()
}
2. Forgetting Type-Only Imports
// ❌ WRONG: Runtime import for typing
import { UserRole } from '@prisma/client'
interface UserData {
role: UserRole // This pulls in runtime dependency
}
// ✅ CORRECT: Type-only import
import type { UserRole } from '@prisma/client'
interface UserData {
role: UserRole // Only uses the type
}
Framework-Specific Considerations
In framework applications, be especially careful with:
For Next.js:
- App Router has stricter server/client boundaries
- Use
'use client'directive carefully when dealing with enums
For SvelteKit:
- Server-only modules vs client-side code
- Universal load functions
For other frameworks:
- Auto-imported composables that might use Prisma enums
- Server-side rendering contexts
- Universal code that runs on both server and client
Debugging Tips
1. Check Your Imports
Search your codebase for problematic imports:
# Find direct Prisma client imports in client code
grep -r "from '@prisma/client'" src/components src/pages src/composables
# Look for non-type imports
grep -r "import {.*} from '@prisma/client'" src/
2. Build Analysis
Most bundlers provide build analysis:
# Analyze what's being bundled
npm run build:analyze
# Look for unexpected Prisma dependencies in client bundle
3. Enable Verbose Logging
// vite.config.js or build config
export default {
build: {
rollupOptions: {
logLevel: 'debug',
},
},
}
Performance Benefits
Beyond fixing build errors, this pattern provides performance benefits:
- Smaller Bundle Size: Avoids including server dependencies in client bundles
- Faster Build Times: Reduces complex dependency resolution
- Better Tree Shaking: Constants are easier for bundlers to optimize
Migration Strategy
If you have an existing codebase with this issue:
1. Identify Problem Areas
# Find all direct Prisma imports
grep -r "from '@prisma/client'" src/ --include="*.ts" --include="*.vue" --include="*.tsx"
2. Create Constants First Start by creating all necessary constant files.
3. Migrate Incrementally Replace imports file by file, testing builds along the way.
4. Add Linting Rules Prevent regression with ESLint rules:
// .eslintrc.js
module.exports = {
rules: {
'no-restricted-imports': [
'error',
{
paths: [
{
name: '@prisma/client',
importNames: ['UserRole', 'PostStatus'], // Add your enums
message: 'Use constants from ~/constants/enums instead',
},
],
},
],
},
}
Conclusion
The Prisma enum import issue is a common but solvable problem that stems from the architectural difference between server-side ORMs and client-side bundlers. By establishing clear patterns for enum usage—using type-only imports and runtime constants—you can avoid build failures while maintaining type safety and code clarity.
Key takeaways:
- Never import Prisma enums directly in client-side code
- Use type-only imports for TypeScript typing
- Create typed constants for runtime usage
- Maintain consistency between schema and constants
- Establish team conventions and linting rules
This pattern not only solves the immediate build issue but also creates a more maintainable architecture that clearly separates server-side database concerns from client-side application logic.
By following these practices, you'll build more robust applications that work consistently across development and production environments, while maintaining the full power of TypeScript's type system and Prisma's developer experience.
Hopefully, this helps someone else dealing with similar challenges! ✨