Design wechseln

Next.js + Prisma Komplett-Guide: Von der Konfiguration zur Praxis (inkl. Lösung für Connection-Leaks)

Easton editorial illustration: cache waterfall instrument

Wieder diese rote Zeile im Terminal: Error: Can't reach database server at localhost:5432. Genauer gesagt: FATAL: sorry, too many clients already.

Die Datenbank läuft, die Connection-String stimmt – und trotzdem keine Verbindung? Am Anfang ging alles, nach ein paar Codeänderungen bricht es zusammen. Next.js-Dev-Server neu starten? Hilft nicht. Datenbank neu starten? Kurz Ruhe, dann wieder dasselbe.

Später wurde klar: Das ist einer der klassischen Stolpersteine bei Next.js + Prisma – Connection-Leaks durch Hot Reload. Bei jeder Codeänderung erzeugt Hot Reload eine neue Prisma-Instanz, alte Verbindungen bleiben offen, und irgendwann ist der Connection Pool voll.

Vielleicht kennen Sie das: Datenbank für ein Next.js-Projekt, Schwanken zwischen Prisma, TypeORM und Drizzle; Prisma gewählt, aber Fehler bei der Konfiguration; unsicher bei Eins-zu-Viele oder Viele-zu-Viele im Schema – oder mitten in der Entwicklung fällt die Datenbank aus.

Prisma ist weniger steil als man denkt, die Konfiguration ist überschaubar. Entscheidend sind: Umgebung aufsetzen, Connection-Leaks vermeiden, Schema entwerfen, CRUD schreiben. Genau das ordnet dieser Artikel – mit weniger Umwegen.

Warum Prisma? (Vergleich mit TypeORM und nativem SQL)

Bei Next.js gibt es viele Datenbankoptionen. TypeORM ist etabliert, Drizzle leichtgewichtig, natives SQL maximal performant. Warum trotzdem Prisma?

Typsicherheit – wirklich sicher

Als ich zum ersten Mal die automatisch generierten TypeScript-Typen von Prisma sah, war ich echt beeindruckt – nicht nur „nett“, sondern „so geht das also“.

Schema schreiben, npx prisma generate ausführen – fertig sind Typdefinitionen für alle Modelle. Keine simplen Interfaces, sondern vollständige Typen mit IntelliSense. Bei prisma.user.findUnique({ where: { id: zeigt VSCode Typ und verfügbare Where-Felder.

Vergleich:

  • TypeORM: Viele Decorators (@Entity(), @Column()), Typen und DB-Schema getrennt – leicht desynchron
  • Natives SQL: Ergebnisse oft any, Interfaces von Hand, Schema-Änderungen manuell nachziehen
  • Prisma: Schema als Single Source of Truth, Typen synchron, nach Schema-Änderung neu generieren

Das ist nicht nur Komfort. Typsicherheit fängt Fehler beim Schreiben ab – nicht erst zur Laufzeit.

Developer Experience – Details zählen

Besonders angenehm finde ich:

Prisma Studio: Visuelles DB-UI – npx prisma studio, Daten im Browser ansehen und bearbeiten, ohne TablePlus oder SQL.

Migrationen: prisma migrate dev erzeugt und wendet Migrationen an – einfacher als bei TypeORM. Schema ändern, Prisma erkennt Diff, fragt nach Migration – kein manuelles SQL-Skript.

Query-Syntax: Prisma fühlt sich nach JavaScript an:

const users = await prisma.user.findMany({
  where: { email: { contains: '@gmail.com' } },
  include: { posts: true },
  orderBy: { createdAt: 'desc' }
})

Intuitiv – kein SQL, keine Decorator-Syntax.

TypeORM QueryBuilder zum Vergleich:

const users = await userRepository.createQueryBuilder("user")
  .where("user.email LIKE :email", { email: "%@gmail.com%" })
  .leftJoinAndSelect("user.posts", "posts")
  .orderBy("user.createdAt", "DESC")
  .getMany()

Gleiche Funktion – Prisma klarer und fehlerärmer.

Performance und Ökosystem

„ORM ist langsam – in Produktion natives SQL?“ – ein Mythos. Prisma hat Overhead, für die meisten Projekte vernachlässigbar. Optimierungen:

  • N+1: Mit include werden Abfragen zusammengeführt
  • Connection Pool: Sinnvolle Defaults, z. B. num_cpus * 2 + 1
  • Feldauswahl: select reduziert übertragene Daten

Engpass? Natives SQL geht auch:

const result = await prisma.$queryRaw`SELECT * FROM User WHERE id = ${userId}`

ORM-Komfort plus SQL dort, wo es zählt.

Ökosystem: 38k+ GitHub-Stars, Vercel-Empfehlung, starke Doku. Next.js-Doku hat eine Prisma-Integration – mainstream-tauglich.

Prisma ist nicht perfekt, aber für die meisten Next.js-Full-Stack-Projekte überwiegen Typsicherheit, DX und Ökosystem.

Umgebung einrichten: Prisma von null konfigurieren

Prisma gewählt – als Nächstes die Umgebung. In etwa zehn Minuten erledigt.

Abhängigkeiten installieren und initialisieren

Next.js-Projekt vorausgesetzt – sonst npx create-next-app@latest.

Prisma installieren:

npm install prisma @prisma/client

Zwei Pakete:

  • prisma: CLI (Init, Migrationen, Studio)
  • @prisma/client: Client für DB-Zugriff

Initialisieren:

npx prisma init

Erstellt:

  1. prisma/schema.prisma (Kernkonfiguration)
  2. .env mit DATABASE_URL

Datenbankverbindung konfigurieren

In .env steht typischerweise:

DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"

PostgreSQL-Format:

postgresql://Benutzer:Passwort@Host:Port/Datenbank?schema=public

Lokal empfiehlt sich PostgreSQL – vollständig, beste Prisma-Unterstützung, Produktion oft ebenfalls PostgreSQL.

Kein PostgreSQL? Docker ist am schnellsten:

docker-compose.yml:

version: '3.8'
services:
  postgres:
    image: postgres:15
    restart: always
    environment:
      POSTGRES_USER: myuser
      POSTGRES_PASSWORD: mypassword
      POSTGRES_DB: mydb
    ports:
      - '5432:5432'
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

Starten:

docker-compose up -d

.env anpassen:

DATABASE_URL="postgresql://myuser:mypassword@localhost:5432/mydb?schema=public"

Wichtig: .env nicht committen – in .gitignore:

.env
.env.local

MySQL oder SQLite – leicht andere URLs:

MySQL:

DATABASE_URL="mysql://root:password@localhost:3306/mydb"

SQLite (Datei-DB für kleine Projekte):

DATABASE_URL="file:./dev.db"

Prisma Client generieren

Nach der Verbindung:

npx prisma generate

Liest schema.prisma, erzeugt Typen und Query-Methoden in node_modules/@prisma/client.

Nach jeder Schema-Änderung erneut npx prisma generate – sonst TypeScript-Fehler.

Umgebung steht. Als Nächstes: Connection-Leaks beim Hot Reload – ein Schwerpunkt vieler Fehltritte.

Connection-Leaks durch Hot Reload lösen (Schwerpunkt)

Jetzt das Problem, das viele verzweifeln lässt: FATAL: sorry, too many clients already.

Was passiert?

Im Dev-Modus nutzt Next.js Hot Module Replacement (HMR): Code speichern, Seite aktualisiert sich – ohne Server-Neustart.

Mit Prisma kollidiert das.

Bei jedem Reload werden Module neu geladen. Direktes new PrismaClient() in API Routes oder Server Components → neue Instanz pro Reload.

Jede Instanz öffnet DB-Verbindungen; alte bleiben hängen.

PostgreSQL erlaubt standardmäßig ~100 Verbindungen. Nach einigen Reloads: 10, 20, 50, 100 – voll. too many clients already.

Ich war ratlos – DB lief, kurz nach Neustart wieder kaputt. In Prisma GitHub Issue #10247 berichten viele dasselbe.

Singleton-Muster: die Lösung

Singleton – nur eine PrismaClient-Instanz pro App.

Instanz auf globalThis legen. globalThis überlebt Hot Reload. Erste Instanz wird wiederverwendet, keine neuen Verbindungen.

lib/prisma.ts im Projektroot:

import { PrismaClient } from '@prisma/client'

const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClient | undefined
}

export const prisma = globalForPrisma.prisma || new PrismaClient({
  log: ['query', 'error', 'warn'], // Alle Queries in der Entwicklung sichtbar
})

if (process.env.NODE_ENV !== 'production') {
  globalForPrisma.prisma = prisma
}

Erklärung:

  1. globalForPrisma typisiert globalThis für TypeScript
  2. globalForPrisma.prisma || new PrismaClient() – vorhandene Instanz nutzen oder neu anlegen
  3. NODE_ENV !== 'production' – Caching nur in Dev; Produktion hat kein HMR

Überall prisma aus dieser Datei importieren:

App Router (Next.js 13+) Server Component:

// app/users/page.tsx
import { prisma } from '@/lib/prisma'

export default async function UsersPage() {
  const users = await prisma.user.findMany()
  
  return (
    <div>
      {users.map(user => (
        <div key={user.id}>{user.name}</div>
      ))}
    </div>
  )
}

API Route:

// app/api/users/route.ts
import { NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'

export async function GET() {
  const users = await prisma.user.findMany()
  return NextResponse.json(users)
}

Pages Router (Next.js 12 und älter):

// pages/api/users.ts
import type { NextApiRequest, NextApiResponse } from 'next'
import { prisma } from '@/lib/prisma'

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  const users = await prisma.user.findMany()
  res.status(200).json(users)
}

Niemals direkt new PrismaClient() – immer aus lib/prisma.ts importieren.

Produktion: kein Problem

Auf Vercel o. Ä. gibt es kein HMR; Builds sind statisch. NODE_ENV !== 'production' sorgt für Singleton in Dev, normales Verhalten in Prod.

Offizielle Prisma-Empfehlung – in der Doku dokumentiert.

Der Stolperstein ist verbreitet, die Lösung simpel: lib/prisma.ts einmal setzen, Leaks vergessen.

Schema-Design – Best Practices

Umgebung steht, Leaks behoben – Tabellen in prisma/schema.prisma definieren.

Basis-Modelle

Blog-Beispiel User und Post:

// prisma/schema.prisma

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String?
  password  String
  posts     Post[]
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

model Post {
  id        Int      @id @default(autoincrement())
  title     String
  content   String?
  published Boolean  @default(false)
  authorId  Int
  author    User     @relation(fields: [authorId], references: [id])
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

Wichtige Attribute:

  • @id: Primärschlüssel
  • @default(autoincrement()): Auto-Increment
  • @unique: Eindeutigkeit (z. B. email)
  • String?: Optional/nullable
  • @default(now()): Zeitstempel bei Erstellung
  • @updatedAt: Automatisches Update-Datum

Namenskonvention:

  • Modelle PascalCase: User, Post, UserProfile
  • Felder camelCase: createdAt, authorId
  • Tabellennamen: Prisma mappt zu Kleinbuchstaben Plural (Userusers)

Beziehungen: Eins-zu-Viele, Viele-zu-Viele, Eins-zu-Eins

Eins-zu-Viele (One-to-Many)

Ein User, viele Posts; ein Post, ein User.

  • User: posts Post[]
  • Post: author User, authorId Int

@relation(fields: [authorId], references: [id]):

  • fields: [authorId] – Fremdschlüssel in Post
  • references: [id] – referenziert User.id

Viele-zu-Viele (Many-to-Many)

Post mit vielen Tags, Tag in vielen Posts.

Implizit (Prisma verwaltet Zwischentabelle):

model Post {
  id    Int    @id @default(autoincrement())
  title String
  tags  Tag[]
}

model Tag {
  id    Int    @id @default(autoincrement())
  name  String
  posts Post[]
}

Prisma legt _PostToTag an; Abfrage mit include: { tags: true }.

Explizite Zwischentabelle (Extra-Felder möglich):

model Post {
  id       Int        @id @default(autoincrement())
  title    String
  postTags PostTag[]
}

model Tag {
  id       Int        @id @default(autoincrement())
  name     String
  postTags PostTag[]
}

model PostTag {
  id        Int      @id @default(autoincrement())
  postId    Int
  tagId     Int
  post      Post     @relation(fields: [postId], references: [id])
  tag       Tag      @relation(fields: [tagId], references: [id])
  createdAt DateTime @default(now())
  
  @@unique([postId, tagId])
}

Z. B. createdAt auf der Verknüpfung – sonst reicht implizit.

Eins-zu-Eins (One-to-One)

User mit einem Profile:

model User {
  id      Int      @id @default(autoincrement())
  email   String   @unique
  profile Profile?
}

model Profile {
  id     Int    @id @default(autoincrement())
  bio    String?
  userId Int    @unique
  user   User   @relation(fields: [userId], references: [id])
}

userId mit @unique – ein Profile pro User.

Fortgeschritten

Enum:

enum Role {
  USER
  ADMIN
  MODERATOR
}

model User {
  id    Int    @id @default(autoincrement())
  email String @unique
  role  Role   @default(USER)
}

Zusammengesetzter Unique-Index:

model Post {
  id       Int    @id @default(autoincrement())
  title    String
  authorId Int
  slug     String
  
  @@unique([authorId, slug])
}

Mehrere Relationen zwischen Modellenname:

model User {
  id             Int    @id @default(autoincrement())
  writtenPosts   Post[] @relation("PostAuthor")
  favoritePosts  Post[] @relation("PostFavorites")
}

model Post {
  id          Int    @id @default(autoincrement())
  title       String
  authorId    Int
  author      User   @relation("PostAuthor", fields: [authorId], references: [id])
  favoritedBy User[] @relation("PostFavorites")
}

Ohne name ist die Zuordnung mehrdeutig.

Vollständiges Blog-Schema

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

enum Role {
  USER
  ADMIN
}

model User {
  id        Int       @id @default(autoincrement())
  email     String    @unique
  name      String?
  password  String
  role      Role      @default(USER)
  posts     Post[]
  comments  Comment[]
  profile   Profile?
  createdAt DateTime  @default(now())
  updatedAt DateTime  @updatedAt
}

model Profile {
  id     Int     @id @default(autoincrement())
  bio    String?
  avatar String?
  userId Int     @unique
  user   User    @relation(fields: [userId], references: [id])
}

model Post {
  id        Int       @id @default(autoincrement())
  title     String
  content   String?
  published Boolean   @default(false)
  authorId  Int
  author    User      @relation(fields: [authorId], references: [id])
  comments  Comment[]
  tags      Tag[]
  createdAt DateTime  @default(now())
  updatedAt DateTime  @updatedAt
}

model Comment {
  id        Int      @id @default(autoincrement())
  content   String
  postId    Int
  post      Post     @relation(fields: [postId], references: [id])
  authorId  Int
  author    User     @relation(fields: [authorId], references: [id])
  createdAt DateTime @default(now())
}

model Tag {
  id    Int    @id @default(autoincrement())
  name  String @unique
  posts Post[]
}

npx prisma migrate dev --name init – Tabellen, Indizes, Fremdschlüssel.

Schema-Design: drei Beziehungstypen, @relation, Enums, Unique-Indizes – reicht für die meisten Fälle.

CRUD in der Praxis

Schema steht – Daten per Code. Prisma-API ist selbsterklärend.

Immer prisma aus lib/prisma.ts importieren.

Create

Einzelner Datensatz:

import { prisma } from '@/lib/prisma'

const user = await prisma.user.create({
  data: {
    email: '[email protected]',
    name: 'Alice',
    password: 'hashed_password_here'
  }
})

Bulk:

const users = await prisma.user.createMany({
  data: [
    { email: '[email protected]', name: 'Bob', password: 'pass1' },
    { email: '[email protected]', name: 'Charlie', password: 'pass2' }
  ],
  skipDuplicates: true
})

console.log(`${users.count} Benutzer erstellt`)

Verschachtelt (User + Profile):

const user = await prisma.user.create({
  data: {
    email: '[email protected]',
    name: 'Dave',
    password: 'pass',
    profile: {
      create: {
        bio: 'Entwickler mit Leidenschaft für Code'
      }
    }
  },
  include: {
    profile: true
  }
})

Read

Einzeln:

const user = await prisma.user.findUnique({
  where: { email: '[email protected]' }
})

const firstPost = await prisma.post.findFirst({
  where: { published: true },
  orderBy: { createdAt: 'desc' }
})

Mehrere:

const users = await prisma.user.findMany({
  where: {
    email: {
      contains: '@gmail.com'
    }
  },
  orderBy: { createdAt: 'desc' },
  take: 10,
  skip: 0
})

Relationen:

include:

const user = await prisma.user.findUnique({
  where: { id: 1 },
  include: {
    posts: true,
    profile: true
  }
})

select (performanter):

const user = await prisma.user.findUnique({
  where: { id: 1 },
  select: {
    id: true,
    email: true,
    posts: {
      select: {
        id: true,
        title: true
      }
    }
  }
})

include lädt alle Felder; select nur Gewählte – bei großen Datenmengen spürbar.

Filter:

const posts = await prisma.post.findMany({
  where: {
    OR: [
      { title: { contains: 'Next.js' } },
      { content: { contains: 'Prisma' } }
    ],
    AND: [
      { published: true },
      { authorId: { not: 1 } }
    ]
  }
})

Operatoren: equals, not, in, notIn, contains, startsWith, endsWith, gt/gte, lt/lte.

Update

Einzeln:

const user = await prisma.user.update({
  where: { id: 1 },
  data: { name: 'Alice Updated' }
})

Mehrere:

const result = await prisma.user.updateMany({
  where: { email: { contains: '@gmail.com' } },
  data: { role: 'ADMIN' }
})

console.log(`${result.count} Benutzer aktualisiert`)

Upsert:

const user = await prisma.user.upsert({
  where: { email: '[email protected]' },
  update: { name: 'Alice Updated' },
  create: {
    email: '[email protected]',
    name: 'Alice',
    password: 'pass'
  }
})

Delete

Einzeln:

const user = await prisma.user.delete({
  where: { id: 1 }
})

Mehrere:

const result = await prisma.user.deleteMany({
  where: {
    createdAt: {
      lt: new Date('2023-01-01')
    }
  }
})

console.log(`${result.count} Benutzer gelöscht`)

Transaktionen

Mehrere Schritte atomar – z. B. Überweisung:

Array-Transaktion:

const [user, post] = await prisma.$transaction([
  prisma.user.create({ data: { email: '[email protected]', password: 'pass' } }),
  prisma.post.create({ data: { title: 'Test Post', authorId: 1 } })
])

Interaktiv:

const transferMoney = await prisma.$transaction(async (tx) => {
  const accountA = await tx.account.update({
    where: { id: 1 },
    data: { balance: { decrement: 100 } }
  })
  
  if (accountA.balance < 0) {
    throw new Error('Insufficient balance')
  }
  
  const accountB = await tx.account.update({
    where: { id: 2 },
    data: { balance: { increment: 100 } }
  })
  
  return { accountA, accountB }
})

Fehler → Rollback, Salden unverändert.

Vollständiges CRUD-API-Beispiel

// app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'

export async function GET(request: NextRequest) {
  try {
    const { searchParams } = new URL(request.url)
    const page = parseInt(searchParams.get('page') || '1')
    const limit = parseInt(searchParams.get('limit') || '10')
    
    const posts = await prisma.post.findMany({
      where: { published: true },
      include: {
        author: {
          select: { id: true, name: true, email: true }
        },
        tags: true
      },
      orderBy: { createdAt: 'desc' },
      skip: (page - 1) * limit,
      take: limit
    })
    
    const total = await prisma.post.count({ where: { published: true } })
    
    return NextResponse.json({ posts, total, page, limit })
  } catch (error) {
    return NextResponse.json({ error: 'Failed to fetch posts' }, { status: 500 })
  }
}

export async function POST(request: NextRequest) {
  try {
    const body = await request.json()
    const { title, content, authorId, tagIds } = body
    
    const post = await prisma.post.create({
      data: {
        title,
        content,
        authorId,
        tags: {
          connect: tagIds.map((id: number) => ({ id }))
        }
      },
      include: { tags: true }
    })
    
    return NextResponse.json(post, { status: 201 })
  } catch (error) {
    return NextResponse.json({ error: 'Failed to create post' }, { status: 500 })
  }
}
// app/api/posts/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'

export async function GET(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  try {
    const post = await prisma.post.findUnique({
      where: { id: parseInt(params.id) },
      include: {
        author: { select: { id: true, name: true } },
        tags: true,
        comments: {
          include: {
            author: { select: { id: true, name: true } }
          }
        }
      }
    })
    
    if (!post) {
      return NextResponse.json({ error: 'Post not found' }, { status: 404 })
    }
    
    return NextResponse.json(post)
  } catch (error) {
    return NextResponse.json({ error: 'Failed to fetch post' }, { status: 500 })
  }
}

export async function PATCH(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  try {
    const body = await request.json()
    const post = await prisma.post.update({
      where: { id: parseInt(params.id) },
      data: body
    })
    
    return NextResponse.json(post)
  } catch (error) {
    return NextResponse.json({ error: 'Failed to update post' }, { status: 500 })
  }
}

export async function DELETE(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  try {
    await prisma.post.delete({
      where: { id: parseInt(params.id) }
    })
    
    return NextResponse.json({ message: 'Post deleted' })
  } catch (error) {
    return NextResponse.json({ error: 'Failed to delete post' }, { status: 500 })
  }
}

Mit create, findUnique, findMany, update, delete, Filtern, Relationen und Transaktionen decken Sie die meisten Szenarien ab.

Fortgeschrittene Tipps und häufige Fragen

CRUD sitzt – in echten Projekten zählen Performance, Migrationen und Debugging.

Performance

select für weniger Felder

Standard: alle Spalten. Bei großen Textfeldern select:

const posts = await prisma.post.findMany({
  select: {
    id: true,
    title: true,
    createdAt: true,
    author: {
      select: { name: true }
    }
  }
})

Listen brauchen selten vollen content.

N+1 vermeiden

const users = await prisma.user.findMany({
  include: { posts: true }
})

Statt Schleife mit Einzelabfragen pro User.

Connection Pool

Default: num_cpus * 2 + 1. Serverless/Hochlast anpassen:

DATABASE_URL="postgresql://user:password@localhost:5432/mydb?connection_limit=5"

Serverless: klein starten (z. B. 1). Auf Vercel: Prisma Accelerate für HTTP-Pooling und Cache.

Migrationen

Entwicklung – nach Schema-Änderung:

npx prisma migrate dev --name add-user-role

Erkennt Diff, erzeugt SQL, wendet an, regeneriert Client. --name beschreibend wählen.

Produktionnie migrate dev:

npx prisma migrate deploy

Wendet nur vorhandene Migrationen an. In CI/CD vor Deploy ausführen.

Rollback – kein Built-in; npx prisma migrate status, manuelles SQL oder Backup-Restore. Backups in Prod Pflicht.

Debugging

Query-Logs:

export const prisma = new PrismaClient({
  log: ['query', 'info', 'warn', 'error']
})

Prod: log: ['error'].

Prisma Studio:

npx prisma studio

Tabellen ansehen, Datensätze bearbeiten, Relationen testen.

Typische Fehler

P2002: Unique constraint failed → Duplikat (z. B. email).

P2025: Record not found → Update/Delete auf fehlenden Datensatz.

P1001: Can't reach database server → URL, DB-Lauf prüfen.

Vercel-Deployment

In package.json:

{
  "scripts": {
    "build": "prisma generate && next build"
  }
}

DATABASE_URL im Vercel-Dashboard – nicht im Code.

PostgreSQL: Vercel Postgres oder Supabase mit Free-Tier.

Kernpunkte:

  • Performance: select, include, Connection Pool
  • Migrationen: Dev migrate dev, Prod migrate deploy
  • Debug: Logs, Studio, Fehlercodes
  • Deploy: prisma generate im Build, Env-Vars setzen

Fazit

Von Setup über CRUD bis Connection-Leaks und Schema – ein Durchlauf durch Next.js + Prisma.

Noch einmal die wichtigsten Punkte:

Connection-Leaks: lib/prisma.ts mit Singleton, überall importieren – Pflicht in der Entwicklung.

Schema: Eins-zu-Viele, Viele-zu-Viele, Eins-zu-Eins und @relation; einheitliche Namen, Enums nutzen.

CRUD: create, findMany, update, delete; include vs. select; $transaction für Konsistenz.

Performance & Deploy: select sparsam, N+1 vermeiden, Migrationen nach Umgebung trennen, auf Vercel prisma generate.

Prisma ist zugänglicher als rohes SQL, mit starker Typsicherheit und DX – Overhead gibt es, komplexe Queries manchmal natives SQL. Für die meisten Next.js-Full-Stack-Projekte lohnt es sich.

Als Nächstes:

  • Kleines Next.js + Prisma-Projekt bauen
  • Prisma-Dokumentation für Advanced Features
  • Prisma Studio ausprobieren
  • GitHub Discussions der Community bei Fragen

Teilen Sie gern Erfahrungen und Stolpersteine in den Kommentaren – anderen hilft das oft weiter.

Next.js + Prisma – vollständiger Konfigurationsablauf

Vom Installieren bis zur Behebung von Connection-Leaks, Schema-Design und CRUD-Operationen

⏱️ Estimated time: 4 hr

  1. 1

    Step 1: Prisma installieren und initialisieren

    Abhängigkeiten installieren:
    • npm install prisma @prisma/client
    • npx prisma init

    Die Initialisierung erstellt:
    • prisma/schema.prisma: Schema-Definitionsdatei
    • .env: Umgebungsvariablen (inkl. DATABASE_URL)

    Datenbankverbindung konfigurieren:
    • DATABASE_URL in .env setzen
    • Format: postgresql://user:password@localhost:5432/dbname
  2. 2

    Step 2: Connection-Leak-Problem lösen

    Singleton-Muster anlegen:
    • Datei lib/prisma.ts erstellen
    • In der Entwicklung Instanz über globalThis cachen
    • In Produktion Instanz direkt exportieren

    Code:
    const globalForPrisma = globalThis as unknown as { prisma: PrismaClient }
    export const prisma = globalForPrisma.prisma || new PrismaClient()
    if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma

    So vermeiden Sie mehrere Verbindungen durch Hot Reload
  3. 3

    Step 3: Schema entwerfen

    Modelle definieren:
    • Tabellen mit model definieren
    • Primärschlüssel mit @id markieren
    • Standardwerte mit @default setzen
    • Beziehungen mit @relation definieren

    Beziehungstypen:
    • Eins-zu-Eins: @relation(fields, references)
    • Eins-zu-Viele: Ein Modell mit @relation, das andere ohne
    • Viele-zu-Viele: Zwischentabelle (@relation-Tabelle)

    Migration erzeugen:
    • npx prisma migrate dev --name init
  4. 4

    Step 4: CRUD-Operationen implementieren

    Daten anlegen:
    • prisma.user.create({ data: { name, email } })

    Daten abfragen:
    • prisma.user.findMany(): mehrere Datensätze
    • prisma.user.findUnique({ where: { id } }): ein Datensatz
    • prisma.user.findFirst({ where: { ... } }): erster Treffer

    Daten aktualisieren:
    • prisma.user.update({ where: { id }, data: { name } })

    Daten löschen:
    • prisma.user.delete({ where: { id } })
  5. 5

    Step 5: Relationen abfragen

    include nutzen:
    • prisma.user.findMany({ include: { posts: true } })
    • Gibt Benutzer inkl. aller Beiträge zurück

    select nutzen:
    • prisma.user.findMany({ select: { id: true, name: true } })
    • Nur angegebene Felder zurückgeben, weniger Daten

    N+1-Problem vermeiden:
    • include für Relationen in einer Abfrage
    • Keine Relationen in äußeren Schleifen nachladen
  6. 6

    Step 6: Deployment und Migrationen

    Produktions-Deployment:
    • DATABASE_URL als Umgebungsvariable setzen
    • npx prisma generate ausführen
    • npx prisma migrate deploy für Migrationen

    Vercel-Deployment:
    • DATABASE_URL im Vercel Dashboard setzen
    • prisma generate in den Build-Befehl einbinden
    • prisma migrate deploy für Migrationen

    Hinweis: migrate dev nicht in Produktion ausführen

FAQ

Warum tritt der Fehler ‚too many clients already‘ auf?
Das ist ein Connection-Leak durch Next.js Hot Reload.

Bei jeder Codeänderung entsteht eine neue PrismaClient-Instanz, alte Verbindungen schließen sich nicht automatisch – der Connection Pool ist irgendwann erschöpft.

Lösung: Singleton-Muster; in der Entwicklung PrismaClient über globalThis cachen.
Was unterscheidet Prisma von TypeORM und Drizzle?
Prisma:
• Beste Typsicherheit, beste Developer Experience
• Etwas Performance-Overhead

TypeORM:
• Mächtig, komplexe Queries möglich
• Konfiguration aufwendiger

Drizzle:
• Leichtgewichtig, gute Performance
• Typsicherheit schwächer als bei Prisma

Für Next.js-Projekte überwiegen bei Prisma Einfachheit und Typsicherheit.
Wie entwirft man eine Eins-zu-Viele-Beziehung?
Auf der ‚Viele‘-Seite Fremdschlüsselfeld hinzufügen und mit @relation markieren.

Beispiel: User hat viele Posts
• Im Post-Modell userId und @relation(fields: ['userId'], references: [id])
• Im User-Modell posts Post[]
Was ist der Unterschied zwischen include und select?
include:
• Für Relationen, liefert verknüpfte Daten
• Erhöht Datenmenge der Abfrage

select:
• Wählt Felder aus, nur angegebene Felder
• Reduziert Datenmenge

Kombinierbar: { include: { posts: true }, select: { id: true, name: true } }
Wie vermeidet man N+1-Abfragen?
include nutzen, um Relationen in einer Abfrage zu laden – nicht in äußeren Schleifen. Beispiel: prisma.user.findMany({ include: { posts: true } }) lädt alle User und Posts auf einmal statt pro User einzeln.
Unterstützt Prisma Transaktionen?
Ja. prisma.$transaction([...]) führt mehrere Operationen aus – alles erfolgreich oder alles fehlgeschlagen. Beispiel: await prisma.$transaction([prisma.user.create(...), prisma.post.create(...)])
Wie deployt man Prisma auf Vercel?
Schritte:
1) DATABASE_URL im Vercel Dashboard setzen
2) prisma generate in package.json build einbinden
3) prisma migrate deploy für Migrationen (nicht migrate dev)

Produktions-Datenbankverbindung prüfen.

13 Min. Lesezeit · Veröffentlicht am: 20. Dez. 2025 · Aktualisiert am: 14. Juli 2026

Ähnliche Beiträge

Kommentare

Melde dich mit GitHub an, um einen Kommentar zu hinterlassen

Easton BlogEaston Blog