Next.js API Routes – Komplettguide: Von Route Handlers bis zu Best Practices für Fehlerbehandlung

Letzten Freitagnachmittag kam der Product Manager vorbei: „Können wir eine Benutzerregistrierungs-Schnittstelle hinzufügen?“ Ich öffnete den Ordner pages/api im Projekt, wollte nach dem bekannten Muster loslegen – und stellte fest, dass der Ordner leer war. Dann fiel mir ein: Das ist ein neues Projekt mit App Router, die API-Schreibweise hat sich komplett geändert.
In der Next.js-Dokumentation stand „Route Handlers“ – und ich dachte: schon wieder ein neues Konzept. Einen halben Tag lang habe ich Docs und Beispiele studiert, bis klar war, was route.ts ist und warum die vertrauten req- und res-Objekte nicht mehr gelten.
Wenn Sie bei der Backend-Schnittstellen-Entwicklung in Next.js unsicher sind, bringt dieser Artikel Ordnung ins Thema. Im Vergleich zeige ich, was sich zwischen Pages Router und App Router geändert hat, und anhand praktischer Beispiele, wie Sie Requests verarbeiten, Responses gestalten und Fehler sauber behandeln. Nach dem Lesen können Sie Next.js-Backend-Schnittstellen mit gutem Gefühl entwickeln.
API Routes Grundlagen: Der Wesensunterschied der beiden Schreibweisen
Schreibweise aus der Pages-Router-Ära
Vor Next.js 13 lagen Schnittstellen unter pages/api. Die Syntax erinnerte an Express – mit den Node.js-Objekten req und res:
// pages/api/hello.ts
import type { NextApiRequest, NextApiResponse } from 'next'
export default function handler(
req: NextApiRequest,
res: NextApiResponse
) {
res.status(200).json({ message: 'Hello from Pages Router!' })
}
Vorteil: schneller Einstieg, besonders mit Node.js- oder Express-Erfahrung. Nachteil: Abhängigkeit von Node.js-spezifischen APIs – in Edge-Umgebungen (Edge Runtime) problematisch.
Route Handlers im App Router
Mit Next.js 13 und App Router änderte sich die API-Schreibweise grundlegend. Unter app legen Sie route.ts an und nutzen die Web-Standard-APIs Request und Response:
// app/api/hello/route.ts
export async function GET(request: Request) {
return Response.json({ message: 'Hello from Route Handlers!' })
}
Beim ersten Mal wirkte das ungewohnt. Warum kein req/res mehr? Die Gründe:
- Web-Standards: Native Browser-APIs
RequestundResponse– portabler und zeitgemäß - Bessere Typsicherheit: Stärkere TypeScript-Unterstützung ohne Extra-Typdefinitionen
- Edge Runtime: Deployment auf Vercel Edge, Cloudflare Workers usw. – schnellere Antwortzeiten
Kernunterschiede im Überblick
| Eigenschaft | Pages Router | App Router |
|---|---|---|
| Dateipfad | pages/api/* | app/*/route.ts |
| API-Design | Node.js req/res | Web-Standard Request/Response |
| HTTP-Methoden | Ein Default-Export, manuelles req.method | Separate Exporte pro Methode (GET, POST …) |
| Cache-Verhalten | Kein Cache | GET standardmäßig gecacht |
Anfangs wirkte die Umstellung überflüssig. Nach längerer Nutzung ist die neue Schreibweise klarer – besonders bei verschiedenen HTTP-Methoden ohne lange if (req.method === 'GET')-Ketten.
Route Handlers in der Praxis: HTTP-Requests erstellen und verarbeiten
Unterstützte HTTP-Methoden
Route Handlers unterstützen sieben Methoden: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS. Jede Methode ist eine benannte Export-Funktion – die Struktur zeigt sofort, welche Operationen die Schnittstelle unterstützt.
Beispiel einer vollständigen Benutzerverwaltungs-Schnittstelle:
// app/api/users/route.ts
// Benutzerliste abrufen
export async function GET(request: Request) {
// Query-Parameter aus der URL
const { searchParams } = new URL(request.url)
const page = searchParams.get('page') || '1'
return Response.json({
users: [
{ id: 1, name: 'Anna' },
{ id: 2, name: 'Bernd' }
],
page: parseInt(page)
})
}
// Neuen Benutzer anlegen
export async function POST(request: Request) {
// JSON-Body parsen
const body = await request.json()
return Response.json({
id: 3,
name: body.name
}, { status: 201 })
}
Request-Daten verarbeiten
Next.js Route Handlers bieten mehrere Wege – am Anfang leicht zu verwechseln:
- URL-Parameter:
request.urlmit demURL-Objekt
const { searchParams } = new URL(request.url)
const keyword = searchParams.get('q')
- Request-Body (JSON):
await request.json()
const body = await request.json()
console.log(body.email) // E-Mail-Feld auslesen
- Request-Body (FormData):
await request.formData()
const formData = await request.formData()
const file = formData.get('avatar')
- Header und Cookies: Import aus
next/headers
import { headers, cookies } from 'next/headers'
export async function GET() {
const headersList = headers()
const cookieStore = cookies()
const token = headersList.get('authorization')
const userId = cookieStore.get('user_id')
// ...
}
- Dynamische Route-Parameter: Zweites Funktionsargument
// app/api/users/[id]/route.ts
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const userId = params.id
return Response.json({ userId })
}
Ein Stolperstein: In Next.js 15+ ist params asynchron – dann await params.id nötig. Meist reicht die direkte Nutzung; TypeScript weist darauf hin.
Responses konstruieren
JSON ist der häufigste Fall – mit Response.json():
export async function GET() {
return Response.json({
success: true,
data: { message: 'Vorgang erfolgreich' }
})
}
Statuscode und Header setzen:
export async function POST(request: Request) {
const body = await request.json()
// Validierung fehlgeschlagen → 400
if (!body.email) {
return Response.json(
{ error: 'E-Mail darf nicht leer sein' },
{ status: 400 }
)
}
// Erfolgreich erstellt → 201 mit Headern
return Response.json(
{ id: 123, email: body.email },
{
status: 201,
headers: {
'X-Request-Id': 'abc-123',
'Cache-Control': 'no-cache'
}
}
)
}
Praxisbeispiel: Benutzerregistrierung
Alles zusammen – vollständige Registrierungs-Schnittstelle:
// app/api/auth/register/route.ts
import { headers } from 'next/headers'
export async function POST(request: Request) {
// Request-Header auslesen
const headersList = headers()
const contentType = headersList.get('content-type')
// Content-Type prüfen
if (!contentType?.includes('application/json')) {
return Response.json(
{ error: 'Bitte Daten im JSON-Format senden' },
{ status: 400 }
)
}
// Body parsen
const body = await request.json()
const { username, email, password } = body
// Basisvalidierung
if (!username || !email || !password) {
return Response.json(
{ error: 'Benutzername, E-Mail und Passwort dürfen nicht leer sein' },
{ status: 400 }
)
}
// Hier Benutzer in der Datenbank speichern
// const user = await db.user.create({ username, email, password })
// Erfolgsantwort
return Response.json({
success: true,
data: {
id: 1,
username,
email
}
}, { status: 201 })
}
Das Beispiel deckt Header-Prüfung, JSON-Parsing, Validierung, Fehler- und Erfolgsantwort ab – das Muster für die meisten Schnittstellen.
Fehlerbehandlung: Stabilere und zuverlässigere APIs
Try-Catch richtig einsetzen
Am Anfang packte ich alles in einen großen try-catch:
// ❌ Nicht empfohlen: ein großer try-catch für die gesamte Logik
export async function POST(request: Request) {
try {
const body = await request.json()
// viel Geschäftslogik ...
return Response.json({ success: true })
} catch (error) {
return Response.json({ error: 'Vorgang fehlgeschlagen' }, { status: 500 })
}
}
Problem: Alle Fehler werden 500 – das Frontend bekommt keine Details, Debugging wird mühsam. Besser: pro Operation getrennt behandeln:
// ✅ Empfohlen: Fehlertypen unterscheiden
export async function POST(request: Request) {
let body
// JSON-Parse-Fehler separat
try {
body = await request.json()
} catch (error) {
return Response.json(
{ error: 'Ungültiges Request-Format – JSON prüfen' },
{ status: 400 }
)
}
// Validierungsfehler ohne try-catch
if (!body.email || !body.password) {
return Response.json(
{ error: 'E-Mail und Passwort dürfen nicht leer sein' },
{ status: 400 }
)
}
// Datenbankfehler separat
try {
const user = await db.user.create(body)
return Response.json({ success: true, data: user })
} catch (error) {
// Doppelte E-Mail?
if (error.code === 'P2002') {
return Response.json(
{ error: 'Diese E-Mail ist bereits registriert' },
{ status: 409 }
)
}
// Andere DB-Fehler
console.error('Database error:', error)
return Response.json(
{ error: 'Serverfehler – bitte später erneut versuchen' },
{ status: 500 }
)
}
}
So werden Fehlermeldungen und Statuscodes für das Frontend klar.
Strukturierte Fehlerantworten
In vielen Projekten variiert das Fehlerformat – { error: '...' }, { message: '...' }, { msg: '...' }. Für das Frontend unangenehm.
Ein bewährtes Standardformat:
// Einheitliches Fehlerformat
interface ErrorResponse {
success: false
error: string // Benutzerfreundliche Meldung
code?: string // Fehlercode für i18n
details?: any // Details (nur Entwicklung)
requestId?: string // Request-Tracking-ID
}
// Einheitliches Erfolgsformat
interface SuccessResponse<T> {
success: true
data: T
requestId?: string
}
Hilfsfunktionen dazu:
// lib/api-response.ts
import { nanoid } from 'nanoid'
export function successResponse<T>(data: T, status: number = 200) {
return Response.json({
success: true,
data,
requestId: nanoid()
}, { status })
}
export function errorResponse(
error: string,
status: number = 500,
code?: string,
details?: any
) {
const isDev = process.env.NODE_ENV === 'development'
return Response.json({
success: false,
error,
code,
details: isDev ? details : undefined, // In Produktion keine Details
requestId: nanoid()
}, { status })
}
Schnittstellen werden dadurch schlanker:
// app/api/users/[id]/route.ts
import { successResponse, errorResponse } from '@/lib/api-response'
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const userId = params.id
try {
const user = await db.user.findUnique({ where: { id: userId } })
if (!user) {
return errorResponse('Benutzer nicht gefunden', 404, 'USER_NOT_FOUND')
}
return successResponse(user)
} catch (error) {
return errorResponse(
'Benutzerdaten konnten nicht geladen werden',
500,
'INTERNAL_ERROR',
error
)
}
}
Erwartete vs. unerwartete Fehler
Zwei Kategorien:
- Erwartete Fehler: falsches Format, Ressource fehlt, keine Berechtigung – normale Geschäftslogik, Status 4xx
- Unerwartete Fehler: DB-Verbindung weg, Drittanbieter down, Bug – Systemfehler, Status 5xx
Unterschiedliche Behandlung:
export async function POST(request: Request) {
const body = await request.json()
// Erwarteter Fehler: kein Log nötig
if (!body.email?.includes('@')) {
return errorResponse('Ungültiges E-Mail-Format', 400, 'INVALID_EMAIL')
}
try {
// Externe API
const response = await fetch('https://api.example.com/verify', {
method: 'POST',
body: JSON.stringify({ email: body.email })
})
if (!response.ok) {
// Drittanbieter-Fehler – erwartet
return errorResponse('E-Mail-Verifizierung fehlgeschlagen', 400, 'VERIFICATION_FAILED')
}
return successResponse({ verified: true })
} catch (error) {
// Netzwerk, Timeout – unerwartet
console.error('Unexpected error:', error) // Loggen
// Monitoring, z. B. Sentry
// Sentry.captureException(error)
return errorResponse(
'Dienst vorübergehend nicht verfügbar – bitte später erneut versuchen',
503,
'SERVICE_UNAVAILABLE'
)
}
}
Keine sensiblen Informationen preisgeben
Früher habe ich DB-Fehlermeldungen direkt ans Frontend zurückgegeben – und damit die Tabellenstruktur offengelegt. Richtig:
try {
const user = await db.user.create(body)
return successResponse(user)
} catch (error) {
// ❌ Gefährlich: Roher Fehler
// return errorResponse(error.message, 500)
// ✅ Sicher: Generische Meldung, Details nur im Log
console.error('Database error:', {
error,
userId: request.headers.get('user-id'),
timestamp: new Date().toISOString()
})
return errorResponse(
'Benutzer konnte nicht erstellt werden – bitte später erneut versuchen',
500,
'CREATE_USER_FAILED'
)
}
Entwicklung vs. Produktion:
const isDev = process.env.NODE_ENV === 'development'
return Response.json({
success: false,
error: 'Vorgang fehlgeschlagen',
// Details nur in der Entwicklung
stack: isDev ? error.stack : undefined,
details: isDev ? error : undefined
}, { status: 500 })
Response-Design: Schlüssel zur Frontend-Backend-Zusammenarbeit
RESTful-API-Design
Ich orientiere mich an REST-Prinzipien – nicht dogmatisch, aber die Regeln machen APIs verständlicher.
Kernpunkte:
-
HTTP-Methoden für Aktionen:
- GET: Ressource lesen
- POST: Ressource anlegen
- PUT/PATCH: Ressource aktualisieren
- DELETE: Ressource löschen
-
URLs für Ressourcen:
/api/users– Benutzerliste/api/users/123– Benutzer mit ID 123/api/users/123/posts– Beiträge dieses Benutzers
-
Statuscodes für Ergebnisse:
- 200: Erfolg
- 201: Erfolgreich erstellt
- 400: Client-Parameterfehler
- 401: Nicht angemeldet
- 403: Keine Berechtigung
- 404: Ressource nicht gefunden
- 500: Serverfehler
Beispiel Benutzerverwaltung:
// app/api/users/route.ts
export async function GET(request: Request) {
// GET /api/users?page=1&limit=20
const { searchParams } = new URL(request.url)
const page = parseInt(searchParams.get('page') || '1')
const limit = parseInt(searchParams.get('limit') || '20')
const users = await db.user.findMany({
skip: (page - 1) * limit,
take: limit
})
return Response.json({ success: true, data: users })
}
export async function POST(request: Request) {
// POST /api/users
const body = await request.json()
const user = await db.user.create({ data: body })
return Response.json(
{ success: true, data: user },
{ status: 201 } // Hier 201 verwenden
)
}
// app/api/users/[id]/route.ts
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
// GET /api/users/123
const user = await db.user.findUnique({ where: { id: params.id } })
if (!user) {
return Response.json(
{ success: false, error: 'Benutzer nicht gefunden' },
{ status: 404 }
)
}
return Response.json({ success: true, data: user })
}
export async function PATCH(
request: Request,
{ params }: { params: { id: string } }
) {
// PATCH /api/users/123
const body = await request.json()
const user = await db.user.update({
where: { id: params.id },
data: body
})
return Response.json({ success: true, data: user })
}
export async function DELETE(
request: Request,
{ params }: { params: { id: string } }
) {
// DELETE /api/users/123
await db.user.delete({ where: { id: params.id } })
return Response.json({ success: true, data: null })
}
Einheitliches Response-Format
Ergänzung zum Fehlerkapitel – aktuelles Format:
// Basis-Response-Typ
type ApiResponse<T> =
| { success: true; data: T }
| { success: false; error: string; code?: string }
// Paginierte Antwort
interface PaginatedResponse<T> {
success: true
data: T[]
pagination: {
page: number
limit: number
total: number
totalPages: number
}
}
// Listen-Response-Beispiel
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const page = parseInt(searchParams.get('page') || '1')
const limit = parseInt(searchParams.get('limit') || '20')
const [users, total] = await Promise.all([
db.user.findMany({ skip: (page - 1) * limit, take: limit }),
db.user.count()
])
return Response.json({
success: true,
data: users,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit)
}
})
}
TypeScript-Typdefinitionen
Frontend und Backend teilen sich TypeScript – ein types-Ordner im Projekt:
// types/api.ts
export interface User {
id: string
username: string
email: string
createdAt: string
}
export interface CreateUserRequest {
username: string
email: string
password: string
}
export interface CreateUserResponse {
success: true
data: User
}
// types/api-client.ts
import type { CreateUserRequest, CreateUserResponse } from './api'
export async function createUser(data: CreateUserRequest) {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
})
const result: CreateUserResponse = await response.json()
if (!result.success) {
throw new Error(result.error)
}
return result.data
}
So hat der Frontend-Aufruf vollständige Typunterstützung.
Häufige Probleme und Lösungen
Standard-Caching bei GET-Requests
Der unangenehmste Stolperstein: Benutzerdaten-Schnittstelle lokal ok, nach Deployment alte Daten im Frontend – weil GET standardmäßig gecacht wird.
Next.js geht davon aus, dass viele GET-Antworten statisch sind. In der Praxis sind die meisten GET-Requests dynamisch.
Lösung – eine Zeile in route.ts:
// app/api/users/route.ts
export const dynamic = 'force-dynamic' // Cache deaktivieren
export async function GET() {
const users = await db.user.findMany()
return Response.json({ success: true, data: users })
}
Weitere Optionen:
// Methode 2: revalidate auf 0
export const revalidate = 0
// Methode 3: Cache-Control im Response-Header
export async function GET() {
const users = await db.user.findMany()
return Response.json(
{ success: true, data: users },
{
headers: {
'Cache-Control': 'no-store, max-age=0'
}
}
)
}
Wann Caching sinnvoll ist? Kaum ändernde Daten – Länderliste, Kategorien:
// app/api/countries/route.ts
export const revalidate = 3600 // 1 Stunde Cache
export async function GET() {
const countries = await db.country.findMany()
return Response.json({ success: true, data: countries })
}
API 404 nach Deployment
Lokal alles ok, auf Vercel oder Netlify alles 404 – typische Ursachen:
- Falscher Dateiname: muss
route.tsoderroute.jsheißen, nichtapi.ts - Falscher Pfad:
route.tsnicht im selben Ordner wiepage.tsx - Nicht in Git:
.gitignoreprüfen, Route-Datei committed?
Checkliste:
# ✅ Korrekte Struktur
app/
api/
users/
route.ts # korrekt: GET /api/users
users/
[id]/
route.ts # korrekt: GET /api/users/123
# ❌ Falsche Struktur
app/
api/
users.ts # falsch: sollte route.ts heißen
users/
page.tsx
route.ts # falsch: nicht neben page.tsx
Außerdem: next.config.js darf app nicht ausschließen:
// next.config.js
module.exports = {
// Diese Zeile würde app ignorieren – vermeiden
// pageExtensions: ['page.tsx', 'page.ts'],
}
Redirect in try-catch
Next.js redirect() wirft einen speziellen Fehler – in try-catch wird die Weiterleitung abgefangen:
import { redirect } from 'next/navigation'
// ❌ Falsch
export async function GET() {
try {
const isLoggedIn = await checkAuth()
if (!isLoggedIn) {
redirect('/login') // wird vom catch gefangen
}
return Response.json({ success: true })
} catch (error) {
return Response.json({ error: 'Vorgang fehlgeschlagen' }, { status: 500 })
}
}
// ✅ Richtig
export async function GET() {
const isLoggedIn = await checkAuth()
if (!isLoggedIn) {
redirect('/login') // außerhalb von try-catch
}
try {
const data = await fetchData()
return Response.json({ success: true, data })
} catch (error) {
return Response.json({ error: 'Vorgang fehlgeschlagen' }, { status: 500 })
}
}
In Route Handlers ist redirect() selten nötig – oft reicht Status 401, Frontend übernimmt die Navigation.
Wann Route Handlers nicht nötig sind
Am Anfang dachte ich, jede Datenerfassung braucht eine API. Server Components können Backend-Logik direkt aufrufen – ohne HTTP-Umweg.
Beispiel:
// ❌ Nicht empfohlen: API schreiben, dann aus Komponente aufrufen
// app/api/posts/route.ts
export async function GET() {
const posts = await db.post.findMany()
return Response.json({ success: true, data: posts })
}
// app/blog/page.tsx
async function BlogPage() {
const res = await fetch('http://localhost:3000/api/posts')
const { data } = await res.json()
return <div>{/* Beitragsliste rendern */}</div>
}
// ✅ Empfohlen: Server Component fragt direkt ab
// app/blog/page.tsx
async function BlogPage() {
const posts = await db.post.findMany() // Direkt DB
return <div>{/* Beitragsliste rendern */}</div>
}
Wann Route Handlers?
- Externe Aufrufe: Mobile App, Drittanbieter
- Webhooks: Callbacks externer Dienste
- Datenänderungen aus Client Components: Formulare, Löschen
- Komplexe Logik: Datei-Upload, mehrere externe APIs
Wann nicht?
- Daten in Server Components: direkte DB-Abfrage ist schneller
- Einfache Formulare: Server Actions sind einfacher
- Interne Navigation: Next.js-Routing reicht
Fortgeschrittene Techniken: Professionellere APIs
Eingabevalidierung
Manuelle if-Ketten sind mühsam – ich nutze Zod:
import { z } from 'zod'
// Validierungsregeln
const createUserSchema = z.object({
username: z.string().min(3).max(20),
email: z.string().email(),
password: z.string().min(8),
age: z.number().min(18).optional()
})
export async function POST(request: Request) {
const body = await request.json()
// Validieren
const result = createUserSchema.safeParse(body)
if (!result.success) {
return Response.json({
success: false,
error: 'Validierung fehlgeschlagen',
details: result.error.errors // Detaillierte Validierungsfehler
}, { status: 400 })
}
// result.data ist validiert und typsicher
const user = await db.user.create({ data: result.data })
return Response.json({ success: true, data: user }, { status: 201 })
}
Vorteil von Zod: Validierung und Typen in einem Schritt:
// TypeScript-Typ aus Schema ableiten
type CreateUserInput = z.infer<typeof createUserSchema>
// Entspricht:
// type CreateUserInput = {
// username: string
// email: string
// password: string
// age?: number
// }
Middleware-Muster
Bei mehreren Schnittstellen wiederholt sich Code – Auth, Logging, Fehlerbehandlung. Als Middleware abstrahieren:
// lib/middleware.ts
type RouteHandler = (request: Request, context: any) => Promise<Response>
// Auth-Middleware
export function withAuth(handler: RouteHandler): RouteHandler {
return async (request, context) => {
const token = request.headers.get('authorization')
if (!token) {
return Response.json(
{ success: false, error: 'Nicht angemeldet' },
{ status: 401 }
)
}
// Token prüfen
const user = await verifyToken(token)
if (!user) {
return Response.json(
{ success: false, error: 'Token ungültig' },
{ status: 401 }
)
}
// Benutzerinfo an Handler übergeben
context.user = user
return handler(request, context)
}
}
// Logging-Middleware
export function withLogging(handler: RouteHandler): RouteHandler {
return async (request, context) => {
const start = Date.now()
const { method, url } = request
console.log(`[${method}] ${url} – Start`)
const response = await handler(request, context)
const duration = Date.now() - start
console.log(`[${method}] ${url} – Fertig (${duration}ms)`)
return response
}
}
// Kombiniert
// app/api/profile/route.ts
import { withAuth, withLogging } from '@/lib/middleware'
async function getProfile(request: Request, context: any) {
const user = context.user // Aus Middleware
return Response.json({ success: true, data: user })
}
export const GET = withLogging(withAuth(getProfile))
In Projekten kombiniere ich das mit Zod:
// lib/middleware.ts
export function withValidation<T>(
schema: z.Schema<T>,
handler: (request: Request, data: T, context: any) => Promise<Response>
): RouteHandler {
return async (request, context) => {
const body = await request.json()
const result = schema.safeParse(body)
if (!result.success) {
return Response.json({
success: false,
error: 'Validierung fehlgeschlagen',
details: result.error.errors
}, { status: 400 })
}
return handler(request, result.data, context)
}
}
// Verwendung
export const POST = withAuth(
withValidation(createUserSchema, async (request, data, context) => {
// data ist validiert und typsicher
const user = await db.user.create({ data })
return Response.json({ success: true, data: user }, { status: 201 })
})
)
Edge Runtime wählen
Next.js bietet Node.js Runtime und Edge Runtime. Standard Node.js reicht meist; für globale niedrige Latenz Edge:
// app/api/hello/route.ts
export const runtime = 'edge' // Edge Runtime
export async function GET() {
return Response.json({ message: 'Hello from Edge!' })
}
Vorteile: schnelle Antworten am Edge-Knoten. Einschränkungen:
- Keine Node.js-APIs:
fs,pathusw. nicht verfügbar - Keine klassische DB-Verbindung: HTTP-fähige DBs, z. B. Prisma Data Proxy, PlanetScale
- Bundle-Größe: zu großer Code wird nicht deployed
Wann Edge Runtime?
- Einfache APIs ohne schwere Abhängigkeiten
- Read-heavy Szenarien
- Globale niedrige Latenz
Wann Node.js Runtime?
- Klassische Datenbankverbindung
- Node.js-Ökosystem-Bibliotheken
- Komplexe Geschäftslogik
Die meisten Projekte laufen mit Standard-Node.js; Edge passt für spezifische Fälle.
Fazit
Das Wesentliche zu Next.js API Routes im Überblick:
- Schreibweise: Von Pages Router
req/reszu App RouterRequest/Response– Web-Standards - Route Handlers: Separate Exporte pro HTTP-Methode – klar strukturiert
- Request-Verarbeitung: URL-Parameter, JSON-Body, FormData, Header, Cookies, dynamische Routen
- Fehlerbehandlung: Erwartete vs. unerwartete Fehler, einheitliches Format, keine sensiblen Leaks
- Response-Design: REST-Prinzipien, semantische Statuscodes, geteilte TypeScript-Typen
- Typische Fallen: GET-Cache, 404 nach Deployment, redirect in try-catch, zu viele Route Handlers
Der Wechsel von Pages zu App Router ist spürbar – nach der Gewöhnungsphase sind die Vorteile klar: bessere Typsicherheit, übersichtlicherer Code, flexibleres Deployment.
Als Nächstes:
- Direkt ausprobieren: Eine bestehende Schnittstelle als Route Handler umschreiben
- Vorlage anlegen: Fehlerbehandlung und Response-Format aus diesem Artikel als API-Template sichern
- Weiter lernen: Next.js entwickelt sich schnell – Docs zu Server Actions, Middleware usw. im Blick behalten
Es gibt kein Patentrezept – passen Sie die Ansätze an Ihr Projekt an.
Wenn der Artikel geholfen hat, bookmarken Sie ihn für später. Viel Erfolg mit Next.js!
FAQ
Was sind die Hauptunterschiede zwischen Route Handlers und Pages-Router-API-Routes?
Warum liefert mein GET-Request nicht die neuesten Daten?
Wann Route Handlers und wann Server Components?
Wie behandelt man API-Fehler sauber?
Nach dem Deployment sind alle APIs 404, lokal aber alles ok – was tun?
15 Min. Lesezeit · Veröffentlicht am: 5. Jan. 2026 · Aktualisiert am: 14. Juli 2026
Next.js Komplettleitfaden
Wenn du über die Suche hier gelandet bist, kommst du am schnellsten weiter, indem du zum vorherigen oder nächsten Beitrag dieser Serie springst.
Vorheriger
Next.js Mehrsprachige SEO: Vollständiger Leitfaden – Suchmaschinen indexieren jede Sprache korrekt
Über 60 % der mehrsprachigen Websites haben SEO-Konfigurationsfehler. Dieser Artikel erklärt hreflang-Konfiguration, mehrsprachige Sitemaps, URL-Strategien und typische Fallen – damit jede Sprachversion korrekt rankt.
Teil 17 von 51
Nächster
Next.js API Performance-Optimierung: Caching, Streaming und Edge-Computing in der Praxis
Next.js API-Antwortzeit von 3 Sekunden auf 500 ms senken? Lernen Sie Caching-Strategien, Streaming-Responses und Edge Functions mit echten Codebeispielen und Performance-Vergleichen – so steigern Sie Ihre API-Performance sofort.
Teil 19 von 51
Ähnliche Beiträge
Next.js App Router Einstieg: Kernkonzepte und Grundlagen im Überblick

Next.js App Router Einstieg: Kernkonzepte und Grundlagen im Überblick
Next.js 15 in der Praxis: Wie ich an einem Wochenende ein produktionsreifes Blog-System aufgebaut habe


Kommentare
Melde dich mit GitHub an, um einen Kommentar zu hinterlassen