Kamil Owczarek
Opublikowano

Ukryte niebezpieczeństwo importowania enumów Prismy w kodzie client-side

Autorzy

Budowanie nowoczesnej aplikacji webowej z Prisma ORM i metaframeworkami w rodzaju Next.js czy SvelteKit potrafi doprowadzić do frustrującego błędu builda, który trudno zdebugować. Opisuję problem, na który trafiłem, przyczynę i rozwiązanie, które w końcu zadziałało.

Problem

Na dev wszystko działa bez zarzutu, ale gdy próbujesz zbudować wersję produkcyjną, dostajesz enigmatyczny błąd:

ERROR  Build Error: Could not resolve "../.prisma/client/index-browser"
from "../.prisma/client/index-browser?commonjs-external"

Build się wywala, a Ty zostajesz z pytaniem, co właściwie poszło nie tak. Komunikat błędu nie prowadzi wprost do przyczyny, przez co debugowanie jest wyjątkowo irytujące.

Zrozumieć ten błąd

Błąd pojawia się, gdy Twój bundler (Vite, Webpack, Rollup itd.) próbuje rozwiązać kod klienta Prismy dla środowiska przeglądarki i natrafia na konflikt między server-side a client-side module resolution.

Sedno problemu jest takie, że Prisma Client jest zaprojektowany do działania na serwerze, a nie w przeglądarce. Kiedy importujesz enumy Prismy bezpośrednio w kodzie client-side, bundler próbuje wciągnąć server-only zależności do client bundle'a, co kończy się konfliktami przy rozwiązywaniu modułów.

Analiza przyczyny źródłowej

Dlaczego tak się dzieje?

  1. Architektura Prisma Client: Prisma generuje klienta, który zawiera sterowniki bazodanowe i zależności specyficzne dla serwera
  2. Zdezorientowany bundler: gdy importujesz z @prisma/client w kodzie frontendowym, bundler próbuje rozwiązać te serwerowe zależności pod przeglądarkę
  3. Konflikty w module resolution: przeglądarkowa wersja klienta Prismy (index-browser) nie ma pełnej funkcjonalności serwerowej, więc rozwiązywanie modułów się wykłada

Problematyczny wzorzec

// ❌ 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]

Wygląda niewinnie i działa na dev, bo serwery deweloperskie mają zwykle bardziej pobłażliwe module resolution. Produkcyjne buildy są jednak surowsze i bezlitośnie obnażają tę architektoniczną niespójność.

Rozwiązanie: wzorzec ze stałymi

Rozwiązanie, do którego doszedłem, polega na stworzeniu otypowanych obiektów ze stałymi, które odwzorowują enumy Prismy, ale nie importują samego klienta Prismy.

Krok 1: utwórz otypowane stałe

// 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

Krok 2: używaj stałych w kodzie klienckim

// ✅ 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}`)
}

Krok 3: użycie po stronie serwera zostaje bez zmian

// 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
    },
  })
}

Zaawansowane wzorce

Generyczny helper do enumów

W projektach z dużą liczbą enumów warto napisać generyczny 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 })

Helpery walidacyjne

Zbuduj funkcje walidacyjne w oparciu o swoje stałe:

// 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)
}

Dobre praktyki

1. Ustal jasne zasady importowania

Skonfiguruj reguły lintera albo ustal konwencje zespołowe:

// ✅ 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. Pilnuj spójności

Trzymaj swoje stałe zsynchronizowane ze schematem Prismy:

// 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. Wykorzystaj TypeScript do bezpieczeństwa

Operator satisfies gwarantuje, że Twoje stałe dokładnie odpowiadają enumowi Prismy:

// 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. Uporządkuj stałe logicznie

constants/
  ├── user.constants.ts      # User-related enums
  ├── post.constants.ts      # Post-related enums
  ├── order.constants.ts     # Order-related enums
  └── index.ts               # Re-exports

Typowe pułapki, których warto unikać

1. Importowanie we współdzielonych utilsach

// ❌ 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. Zapominanie o type-only importach

// ❌ 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
}

Kwestie specyficzne dla frameworków

W aplikacjach frameworkowych szczególnie uważaj na:

Next.js:

  • App Router ma ostrzejsze granice server/client
  • Ostrożnie z dyrektywą 'use client', gdy w grę wchodzą enumy

SvelteKit:

  • Moduły server-only kontra kod client-side
  • Uniwersalne funkcje load

Inne frameworki:

  • Auto-importowane composables, które mogą korzystać z enumów Prismy
  • Konteksty server-side renderingu
  • Uniwersalny kod działający i na serwerze, i na kliencie

Wskazówki do debugowania

1. Przejrzyj swoje importy

Przeszukaj kod projektu pod kątem problematycznych importów:

# 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. Analiza builda

Większość bundlerów oferuje analizę builda:

# Analyze what's being bundled
npm run build:analyze

# Look for unexpected Prisma dependencies in client bundle

3. Włącz szczegółowe logowanie

// vite.config.js or build config
export default {
  build: {
    rollupOptions: {
      logLevel: 'debug',
    },
  },
}

Korzyści wydajnościowe

Poza naprawieniem błędów builda ten wzorzec daje też zysk na wydajności:

  1. Mniejszy rozmiar bundle'a: nie wciągasz serwerowych zależności do client bundle'ów
  2. Szybsze buildy: mniej skomplikowanego rozwiązywania zależności
  3. Lepszy tree shaking: stałe są dla bundlerów łatwiejsze do zoptymalizowania

Strategia migracji

Jeśli masz istniejący projekt z tym problemem:

1. Zidentyfikuj problematyczne miejsca

# Find all direct Prisma imports
grep -r "from '@prisma/client'" src/ --include="*.ts" --include="*.vue" --include="*.tsx"

2. Najpierw stwórz stałe Zacznij od utworzenia wszystkich potrzebnych plików ze stałymi.

3. Migruj przyrostowo Podmieniaj importy plik po pliku, po drodze testując buildy.

4. Dodaj reguły lintera Zabezpiecz się przed regresją regułami ESLinta:

// .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',
          },
        ],
      },
    ],
  },
}

Podsumowanie

Problem z importem enumów Prismy jest częsty, ale rozwiązywalny — wynika z architektonicznej różnicy między ORM-ami działającymi po stronie serwera a bundlerami działającymi po stronie klienta. Ustalając jasne wzorce korzystania z enumów — type-only importy plus stałe runtime'owe — unikasz wywalonych buildów, nie tracąc przy tym type safety ani czytelności kodu.

Najważniejsze wnioski:

  1. Nigdy nie importuj enumów Prismy bezpośrednio w kodzie client-side
  2. Do typowania w TypeScripcie używaj type-only importów
  3. Twórz otypowane stałe na potrzeby runtime'u
  4. Pilnuj spójności między schematem a stałymi
  5. Ustal konwencje zespołowe i reguły lintera

Ten wzorzec nie tylko rozwiązuje doraźny problem z buildem, ale też prowadzi do architektury łatwiejszej w utrzymaniu, która wyraźnie oddziela serwerowe sprawy bazodanowe od logiki aplikacji po stronie klienta.

Stosując te praktyki, zbudujesz solidniejsze aplikacje, które zachowują się tak samo na dev i na produkcji, a przy tym w pełni korzystasz z systemu typów TypeScriptu i developer experience Prismy.

Mam nadzieję, że pomoże to komuś, kto mierzy się z podobnymi problemami.