Design wechseln

Next.js 15 in der Praxis: Wie ich an einem Wochenende ein produktionsreifes Blog-System aufgebaut habe

Easton editorial illustration: API gateway workstation

Einleitung

Next.js 15 ist noch frisch – ich habe die Docs mehrfach gelesen und Tutorials geschaut, blieb aber bei Theorie stehen. Server Actions, App Router – alles klar im Kopf, aber in einem echten Projekt? Am Wochenende habe ich aufgehört nur zu schauen und ein Full-Stack-Blog gebaut: Frontend, Backend, Datenbank, Deploy. Zwei Tage später lief das Blog auf Vercel, Lighthouse Performance 96.

Dieser Artikel ist kein Copy-Paste-Tutorial, sondern das Warum, Fallstricke und Entscheidungen. Stack: Next.js 15 + Server Actions + Prisma + PostgreSQL – produktionsreifer Code.

Warum Next.js 15?

Bei der Stack-Wahl habe ich lange gezögert.

Als Next.js 15 kam, las ich in der Community: „Schon wieder ein Update, ich kann nicht mehr.“ Ehrlich: etwas Widerstand – Version 14 saß noch nicht fest. Nach den neuen Features war klar: das Upgrade lohnt sich.

Server Actions: Schluss mit umständlichen API Routes

Beim Full-Stack-Development nervte mich am meisten das Schreiben von API Routes. api/posts/route.ts, POST definieren, Body parsen, Response zurück – immer dasselbe.

Server Actions ändern das Spiel. 'use server' vor die Funktion, in der Komponente aufrufen – fertig. Beim ersten Mal dachte ich: „Backend-Code in der Frontend-Datei?“ Einmal ausprobiert – überzeugt.

Früher für einen Blog-Post:

// Alte Methode: API Route nötig
// app/api/posts/route.ts
export async function POST(request: Request) {
  const body = await request.json()
  // viel Logik...
}
// Frontend: fetch
const response = await fetch('/api/posts', {
  method: 'POST',
  body: JSON.stringify(data)
})

Heute:

// app/actions/post-actions.ts
'use server'
export async function createPost(formData: FormData) {
  const title = formData.get('title')
  const content = formData.get('content')
  // Direkt in die DB – angenehm
  return await prisma.post.create({
    data: { title, content }
  })
}

In der Komponente wie eine lokale Funktion. Bildlich: früher selbst zum Restaurant (API Routes), heute Lieferservice (Server Actions).

Turbopack: Entwicklung deutlich schneller

Next.js 15 macht Turbopack stabil. Offiziell: 76,7 % schnellerer Start, 96,3 % schnellere Updates – klang nach Marketing, in der Praxis stimmt es.

76.7%
Server-Start schneller
Offizielle Testdaten
96.3%
Code-Updates schneller
Hot Reload fast sofort
2 Sek.
Projektstart
30+ Komponenten: von 7–8 s auf unter 2 s

Über 30 Komponenten: Webpack 7–8 Sekunden Start, Turbopack unter 2. Hot Reload fast instant – ein großer Unterschied für die tägliche Arbeit.

Warum dieser Stack für ein Blog?

Drei Gründe für Next.js als Blog-Basis:

SSR/SSG von Haus aus – SEO zählt. Server-Rendering und statische Generierung sind dafür gemacht. Google bekommt vollständiges HTML – deutlich besser als reines Client-Rendering.

Prisma-Typsicherheit – Mit Mongoose für MongoDB Typen manuell pflegen, schnell Bugs. Prisma generiert TypeScript aus dem Schema – IntelliSense, weniger Fehler.

Server Actions vereinfachen – Keine API Routes, mindestens 30 % weniger Code. Klassisch wären mehrere route.ts-Dateien nötig gewesen.

Tech-Stack und Architektur

Nach dem Warum kommt das Wie.

Mein vollständiger Stack

  • Frontend: Next.js 15 + TypeScript + Tailwind CSS
  • Backend: Next.js Server Actions + NextAuth.js
  • Datenbank: PostgreSQL + Prisma ORM
  • Deploy: Vercel

Warum nicht MongoDB? Am Anfang dachte ich daran – flexibler. Prisma unterstützt PostgreSQL besser; für Artikel, Nutzer, Kommentare passt relational oft besser.

Verzeichnisstruktur

my-blog/
├── app/
│   ├── (auth)/          # Auth-Seiten
│   ├── blog/            # Blog-Seiten
│   │   └── [slug]/      # Dynamische Route
│   ├── dashboard/       # Dashboard
│   ├── actions/         # Server Actions zentral
│   └── api/auth/        # NextAuth
├── components/          # Wiederverwendbare Komponenten
├── lib/
│   ├── prisma.ts        # Prisma-Singleton (wichtig!)
│   └── utils.ts         # Hilfsfunktionen
├── prisma/
│   └── schema.prisma    # DB-Schema
└── public/              # Statische Assets

Mehrere Iterationen. Anfangs Server Actions in Page-Dateien verstreut – unübersichtlich. Ein actions-Ordner macht alles klarer.

Datenbank-Schema

Hier habe ich einen großen Fehler gemacht. Version 1: zu komplex – Tags, Kategorien, Lese-Statistiken – am Ende ungenutzt, halber Tag zum Vereinfachen.

Finales Kern-Schema:

// prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}
model User {
  id        String   @id @default(cuid())
  email     String   @unique
  name      String?
  image     String?
  posts     Post[]
  createdAt DateTime @default(now())
}
model Post {
  id          String   @id @default(cuid())
  title       String
  slug        String   @unique
  content     String   @db.Text
  published   Boolean  @default(false)
  authorId    String
  author      User     @relation(fields: [authorId], references: [id])
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt
  @@index([slug])
  @@index([authorId])
}

Die beiden @@index sind Performance-kritisch. Listen nach slug, Profile nach authorId – mit Index deutlich schneller.

Kernfunktionen im Detail

Architektur steht – jetzt Code.

3.1 Umgebung und Initialisierung

Next.js 15-Projekt anlegen:

npx create-next-app@latest my-blog
cd my-blog
npm install prisma @prisma/client zod next-auth

Prisma initialisieren:

npx prisma init

Erzeugt prisma/schema.prisma und .env. PostgreSQL-Verbindung:

DATABASE_URL="postgresql://username:password@localhost:5432/myblog?schema=public"

Lokal PostgreSQL per Docker:

docker run --name blog-postgres -e POSTGRES_PASSWORD=mypassword -p 5432:5432 -d postgres

3.2 Prisma-Integration

Nach dem Schema Migration:

npx prisma migrate dev --name init

Wichtiger Stolperstein: Prisma-Client-Singleton. Beim ersten Vercel-Deploy vergessen – nach Stunden „Too many connections“, kurz Panik wegen vermeintlichem Angriff.

Ursache: Hot Reload erzeugt neue Prisma-Clients, Connection Pool explodiert. Lösung:

// lib/prisma.ts
import { PrismaClient } from '@prisma/client'
const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClient | undefined
}
export const prisma = globalForPrisma.prisma ?? new PrismaClient()
if (process.env.NODE_ENV !== 'production') {
  globalForPrisma.prisma = prisma
}

Wirkt komplex, garantiert aber eine Instanz in der Entwicklung. Offizielle Prisma-Best Practice.

3.3 Server Actions in der Praxis

Artikel anlegen – Kernfeature:

// app/actions/post-actions.ts
'use server'
import { prisma } from '@/lib/prisma'
import { revalidatePath } from 'next/cache'
import { z } from 'zod'
// Zod-Validierung für Typsicherheit
const PostSchema = z.object({
  title: z.string().min(1, 'Titel darf nicht leer sein').max(100),
  content: z.string().min(10, 'Inhalt mindestens 10 Zeichen'),
  slug: z.string().regex(/^[a-z0-9-]+$/, 'Slug: nur Kleinbuchstaben, Ziffern, Bindestriche')
})
export async function createPost(formData: FormData) {
  const validatedFields = PostSchema.safeParse({
    title: formData.get('title'),
    content: formData.get('content'),
    slug: formData.get('slug')
  })
  if (!validatedFields.success) {
    return { error: 'Validierung fehlgeschlagen' }
  }
  try {
    const post = await prisma.post.create({
      data: {
        ...validatedFields.data,
        authorId: 'user-id-here' // In Produktion aus Session
      }
    })
    revalidatePath('/blog')
    return { success: true, post }
  } catch (error) {
    return { error: 'Artikel konnte nicht erstellt werden' }
  }
}

In der Komponente:

// app/dashboard/new-post/page.tsx
import { createPost } from '@/app/actions/post-actions'
export default function NewPost() {
  return (
    <form action={createPost}>
      <input name="title" placeholder="Titel" />
      <textarea name="content" placeholder="Inhalt" />
      <input name="slug" placeholder="URL-Slug" />
      <button type="submit">Veröffentlichen</button>
    </form>
  )
}

Kein useState, kein fetch, kein onSubmit. Das Formular ruft die Server Action auf – Next.js übernimmt Serialisierung, Request und Fehler.

Unterschied zu API Routes: Server Actions = Lieferservice, API Routes = selbst abholen. Meist reichen Server Actions.

3.4 Authentifizierung

NextAuth.js – überschaubar:

// app/api/auth/[...nextauth]/route.ts
import NextAuth from 'next-auth'
import GitHubProvider from 'next-auth/providers/github'
import { PrismaAdapter } from '@auth/prisma-adapter'
import { prisma } from '@/lib/prisma'
export const authOptions = {
  adapter: PrismaAdapter(prisma),
  providers: [
    GitHubProvider({
      clientId: process.env.GITHUB_ID!,
      clientSecret: process.env.GITHUB_SECRET!
    })
  ]
}
const handler = NextAuth(authOptions)
export { handler as GET, handler as POST }

GitHub OAuth: App in GitHub Developer Settings, Client ID und Secret in .env. Ca. 10 Minuten – deutlich weniger Aufwand als eigenes JWT.

3.5 SSR/SSG-Optimierung

Performance-Kern. Anfangs alles SSR – jeder Listenaufruf hit die DB, unnötig. Dann Mix-Strategie:

Blog-Liste – SSG + ISR:

// app/blog/page.tsx
import { prisma } from '@/lib/prisma'
export const revalidate = 3600
export default async function BlogList() {
  const posts = await prisma.post.findMany({
    where: { published: true },
    orderBy: { createdAt: 'desc' }
  })
  return (
    <div>
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.content.slice(0, 150)}...</p>
        </article>
      ))}
    </div>
  )
}

Statisches HTML beim Build, stündlich Regenerierung – Besucher bekommen schnelle statische Dateien.

Artikel-Detail – dynamisches SSR:

// app/blog/[slug]/page.tsx
export default async function Post({ params }: { params: { slug: string } }) {
  const post = await prisma.post.findUnique({
    where: { slug: params.slug }
  })
  if (!post) notFound()
  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  )
}

Jeder Aufruf: aktuellster Inhalt aus der DB.

Performance-Vergleich:

~200 ms
SSG-Ladezeit
Statisch, am schnellsten
~800 ms
SSR-Ladezeit
Server-Rendering für dynamische Inhalte
~1500 ms
Reines CSR
Client-Rendering, schlechteres SEO

Der Unterschied ist klar – deshalb eignet sich Next.js gut für Blogs: SEO und Performance.

Deploy und Optimierung

Code fertig – Deploy.

Vercel-Workflow

Code nach GitHub, Repository in Vercel importieren. Next.js wird erkannt, fast alles automatisch. Manuell: Umgebungsvariablen:

DATABASE_URL="Produktions-DB-URL"
GITHUB_ID="OAuth Client ID"
GITHUB_SECRET="OAuth Secret"
NEXTAUTH_URL="https://your-domain.vercel.app"
NEXTAUTH_SECRET="Zufallsstring"

Produktions-DB: Supabase Free PostgreSQL, 500 MB/Monat – für persönliche Blogs genug.

Deploy – ca. 2 Minuten live. Beim ersten Aufruf der eigenen Domain kurz innehalten – Screenshot wert.

Produktions-Optimierung

Deploy ist nicht das Ende:

Prisma Connection Pool:

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

Serverless: jeder Aufruf kann neue Verbindung öffnen – directUrl nutzt den Pool.

Bilder:

import Image from 'next/image'
<Image
  src="/avatar.jpg"
  alt="Nutzer-Avatar"
  width={100}
  height={100}
/>

Next.js Image: WebP, Lazy Loading – spürbar schneller.

Finale Lighthouse-Werte:

96 Punkte
Desktop Performance
Mobile 92, SEO 100 – in zwei Tagen von null auf produktionsreif

Lighthouse: Desktop 96, Mobile 92, SEO 100. Zwei Tage von null auf live mit produktionsreifer Performance – das motiviert.

Erweiterungen und Lernressourcen

Aktuell MVP – mögliche nächste Schritte:

  • Markdown-Editor: react-md-editor
  • Kommentare: Giscus (GitHub Discussions)
  • Suche: Algolia oder Prisma-Volltextsuche
  • Dark Mode: mit next-themes einfach

Als Nächstes plane ich einen Markdown-Editor – Artikel schreiben wird angenehmer.

Empfohlene Ressourcen:

Einmal selbst bauen schlägt zehnmal Docs lesen. Theorie hilft – Praxis festigt.

Fazit

Aus dem Wochenende:

  1. Server Actions: Ersetzen die meisten API Routes – spürbar schnellere Entwicklung
  2. Prisma-Typsicherheit: Volles TypeScript – sicheres Coden
  3. SSG/SSR-Flexibilität: Rendering passend zur Seite – Performance und SEO
  4. Vercel-Deploy: Von Code zu live in unter 5 Minuten

Der größte Gewinn war nicht nur Technik, sondern Vertrauen – Full-Stack ist machbar, wenn man anfängt.

Wenn Sie ein eigenes Blog bauen wollen: jetzt starten. Probleme sind normal – ich habe neben der Doku unzählige Stolpersteine überwunden. Wenn Ihr Projekt live geht, lohnt sich jeder Schritt.

Hoffentlich sparen Sie ein paar Umwege. Fragen gerne in den Kommentaren – ich antworte, wenn möglich.

Ich freue mich auf Ihre Projekte!

Produktionsreifes Blog-System mit Next.js 15

Vom Setup bis Deploy: Server Actions, Prisma, SSG/SSR-Optimierung und Kernfunktionen

Estimated time: PT16H

  1. 1

    Step 1: Umgebung und Projekt-Setup

    Next.js 15-Projekt anlegen:
  2. 2

    Step 2: Prisma-Schema konfigurieren

    Kern-Schema entwerfen:
  3. 3

    Step 3: Server Actions implementieren

    Server-Actions-Datei:
  4. 4

    Step 4: Authentifizierung

    NextAuth.js:
  5. 5

    Step 5: SSR/SSG-Strategie

    Blog-Liste SSG+ISR: app/blog/page.tsx mit export const revalidate = 3600, prisma.post.findMany für veröffentlichte Posts, sortiert nach createdAt. Statisches HTML beim Build, stündliche Regenerierung, ~200 ms Ladezeit. Artikel-Detail dynamisches SSR: app/blog/[slug]/page.tsx, prisma.post.findUnique({ where: { slug: params.slug } }), ~800 ms, für häufig aktualisierte Inhalte. Vergleich: SSG ~200 ms, SSR ~800 ms, CSR ~1500 ms.
  6. 6

    Step 6: Vercel-Deploy und Optimierung

    Code nach GitHub pushen. Projekt in Vercel importieren – Next.js automatisch erkannt. Umgebungsvariablen: DATABASE_URL, GITHUB_ID, GITHUB_SECRET, NEXTAUTH_URL (https://your-domain.vercel.app), NEXTAUTH_SECRET. Empfehlung: Supabase Free PostgreSQL (500 MB/Monat). Prisma: directUrl = env(“DIRECT_URL”) in schema.prisma für Serverless-Pool. Bilder: Next.js Image-Komponente, WebP, Lazy Loading. Deploy ~2 Minuten – Lighthouse Desktop 96, Mobile 92, SEO 100.

FAQ

Was ist der Unterschied zwischen Server Actions und API Routes in Next.js 15? Wann nutze ich welche?
Server Actions sind wie „Essen direkt nach Hause geliefert“, API Routes wie „selbst ins Restaurant gehen und abholen“. Ersteres ist bequem, letzteres flexibler.

Vorteile von Server Actions:
1) Bis zu 30 % weniger Code – keine API Routes nötig
2) Direkt in der Komponente aufrufbar – kein useState, fetch oder onSubmit-Handler
3) Next.js übernimmt Serialisierung, Netzwerk-Requests und Fehlerbehandlung
4) Typsicher – TypeScript-Typen direkt nutzbar

Beispiel: Vor der Funktion 'use server' setzen, dann in der Komponente aufrufen (<form action={createPost}>).

Vorteile von API Routes:
• Flexibler bei komplexer Request-Logik
• Middleware-Unterstützung
• Geeignet für benutzerdefinierte HTTP-Antworten

In den meisten Fällen reichen Server Actions – API Routes nur bei komplexer HTTP-Verarbeitung.
Wie groß ist der Performance-Gewinn von Turbopack? Wie fühlt es sich in der Praxis an?
Next.js 15 hat Turbopack vom Experiment in den stabilen Status gehoben.

Offizielle Daten:
• 76,7 % schnellerer lokaler Server-Start
• 96,3 % schnellere Code-Updates

Praxis-Erfahrung:
• Mein Projekt hat über 30 Komponenten – mit Webpack dauerte der Start 7–8 Sekunden, mit Turbopack unter 2 Sekunden
• Code-Änderungen: Hot Reload fast sofort
• Der Unterschied für die Developer Experience ist enorm

Turbopack ist in Rust geschrieben und deutlich schneller als Webpack – besonders in großen Projekten. Wenn Sie noch Webpack nutzen: Upgrade auf Next.js 15 und Turbopack ausprobieren.
Warum ist das Prisma-Client-Singleton wichtig? Was passiert ohne Konfiguration?
Das Prisma-Client-Singleton ist die offizielle Best Practice von Prisma.

Problem:
• In der Next.js-Entwicklung triggert Hot Reload neue Prisma-Client-Instanzen
• Der Connection Pool läuft schnell voll → „Too many connections“
• Beim ersten Vercel-Deploy habe ich das vergessen – nach ein paar Stunden der Fehler, ich dachte kurz an einen DDoS-Angriff

Richtige Konfiguration:
• Globales Singleton in lib/prisma.ts:
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | undefined };
export const prisma = globalForPrisma.prisma ?? new PrismaClient();
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma;
}

So gibt es in der Entwicklung nur eine Prisma-Instanz; Produktion bleibt unverändert. Merken: offizielle Prisma-Best Practice!
Was ist der Unterschied zwischen SSG, SSR und ISR? Wie wähle ich?
SSG (Static Site Generation):
• Statisches HTML beim Build – am schnellsten (~200 ms)
• Für selten ändernde Seiten (Blog-Liste, Über-Seite)

SSR (Server-Side Rendering):
• HTML bei jedem Request auf dem Server
• Für Echtzeit-Daten (Artikel-Detail, Nutzerprofil)
• Ladezeit ca. 800 ms

ISR (Incremental Static Regeneration):
• SSG + zeitgesteuerte Neugenerierung – Geschwindigkeit von SSG, Flexibilität von SSR
• Für gelegentlich aktualisierte Seiten (Blog-Liste stündlich)

Reines CSR (Client-Side Rendering):
• Rendering im Browser – schlechtes SEO, ~1500 ms
• Für Blogs nicht empfohlen

Strategie:
• Blog-Liste: SSG+ISR (export const revalidate = 3600)
• Artikel-Detail: dynamisches SSR
• Über-Seite: reines SSG

Vergleich: SSG ~200 ms, SSR ~800 ms, CSR ~1500 ms – der Unterschied ist klar.
Warum PostgreSQL statt MongoDB? Wie gut unterstützt Prisma PostgreSQL?
Gründe für PostgreSQL:
1) Bessere Prisma-Unterstützung – Typsicherheit, Migrations, Query-Optimierung
2) Relationale DB passt besser zu Artikeln, Nutzern, Kommentaren
3) Starke Volltextsuche für Blog-Suche
4) Stabil in Produktion, solide Transaktionen

Prisma-Vorteile:
• TypeScript-Typen direkt aus dem Schema – vollständige IDE-Unterstützung
• Mit Mongoose für MongoDB musste ich Typen manuell pflegen – fehleranfällig
• Prisma gibt mir beim Coden ein sicheres Gefühl

Für komplexe Beziehungsdaten: PostgreSQL + Prisma ist eine starke Kombination.
Was ist bei Vercel-Deploy von Next.js zu beachten? Wie optimiere ich die Produktion?
Vercel-Deploy:
1) Code nach GitHub pushen
2) Repository in Vercel importieren – Next.js wird automatisch erkannt
3) Umgebungsvariablen (DATABASE_URL, GITHUB_ID, GITHUB_SECRET, NEXTAUTH_URL, NEXTAUTH_SECRET)
4) Deploy – ca. 2 Minuten bis live

Produktions-Optimierung:

1) Prisma Connection Pool:
• In prisma/schema.prisma datasource: directUrl = env("DIRECT_URL")
• Serverless: jeder Aufruf kann neue Verbindung öffnen – directUrl nutzt den Pool

2) Bilder:
• Next.js Image-Komponente – WebP, Lazy Loading, spürbar bessere Performance

3) DB: Supabase Free PostgreSQL (500 MB/Monat – für persönliche Blogs ausreichend)

Ergebnis:
• Lighthouse Desktop Performance 96, Mobile 92, SEO 100
• Zwei Tage von null auf live – produktionsreife Performance

8 Min. Lesezeit · Veröffentlicht am: 24. Nov. 2025 · Aktualisiert am: 14. Juli 2026

Ähnliche Beiträge

Kommentare

Melde dich mit GitHub an, um einen Kommentar zu hinterlassen

Easton BlogEaston Blog