Modul 11: Praxis

Nuxt Server-API bauen

REST-API-Endpunkte direkt in deiner Nuxt-App erstellen

Server-Routen in Nuxt

Nuxt bietet eine integrierte Server-Engine (Nitro), mit der du API-Endpunkte direkt im server/-Verzeichnis definierst. Keine separate Backend-Anwendung nötig — alles in einem Projekt.

Dateien in server/api/ werden automatisch als API-Routen registriert. Die HTTP-Methode wird über den Dateinamen gesteuert:

Dateistruktur → Routen
server/
├── api/
│   ├── kontakte/
│   │   ├── index.get.ts      → GET    /api/kontakte
│   │   ├── index.post.ts     → POST   /api/kontakte
│   │   ├── [id].get.ts       → GET    /api/kontakte/:id
│   │   ├── [id].put.ts       → PUT    /api/kontakte/:id
│   │   └── [id].delete.ts    → DELETE /api/kontakte/:id
│   └── health.get.ts         → GET    /api/health
├── middleware/
│   └── log.ts                → Server-Middleware (alle Requests)
└── utils/
    └── db.ts                 → Geteilte Hilfsfunktionen
🛤️

Rails-Vergleich

In Rails definierst du Routen in config/routes.rb und Controller in app/controllers/. In Nuxt sind Routen und Handler in einer Datei vereint — wie ein minimalistischer Controller pro Endpunkt.

Arbeiten mit JSON-Datendateien

Für Prototypen und kleine Apps reicht eine JSON-Datei als Datenquelle. Wir bauen eine Kontakte-API mit persistenter Dateispeicherung:

server/data/kontakte.json
[
  {
    "id": 1,
    "name": "Anna Schmidt",
    "email": "anna@beispiel.de",
    "telefon": "+49 170 1234567",
    "firma": "TechStart GmbH",
    "rolle": "Entwicklerin"
  },
  {
    "id": 2,
    "name": "Max Weber",
    "email": "max@beispiel.de",
    "telefon": "+49 171 9876543",
    "firma": "WebDesign AG",
    "rolle": "Designer"
  }
]
server/utils/db.ts
import { readFile, writeFile } from 'fs/promises'
import { resolve } from 'path'

export interface Kontakt {
  id: number
  name: string
  email: string
  telefon?: string
  firma?: string
  rolle?: string
}

const DB_PFAD = resolve('./server/data/kontakte.json')

export async function leseKontakte(): Promise<Kontakt[]> {
  try {
    const daten = await readFile(DB_PFAD, 'utf-8')
    return JSON.parse(daten)
  } catch {
    return []
  }
}

export async function schreibeKontakte(kontakte: Kontakt[]): Promise<void> {
  await writeFile(DB_PFAD, JSON.stringify(kontakte, null, 2), 'utf-8')
}

export function naechsteId(kontakte: Kontakt[]): number {
  if (kontakte.length === 0) return 1
  return Math.max(...kontakte.map(k => k.id)) + 1
}

CRUD-Endpunkte implementieren

server/api/kontakte/index.get.ts
export default defineEventHandler(async (event) => {
  const query = getQuery(event)
  let kontakte = await leseKontakte()

  // Suchfilter
  if (query.suche) {
    const suchbegriff = (query.suche as string).toLowerCase()
    kontakte = kontakte.filter(k =>
      k.name.toLowerCase().includes(suchbegriff) ||
      k.email.toLowerCase().includes(suchbegriff) ||
      k.firma?.toLowerCase().includes(suchbegriff)
    )
  }

  return kontakte
})
server/api/kontakte/index.post.ts
export default defineEventHandler(async (event) => {
  const body = await readBody(event)

  // Validierung
  if (!body.name || !body.email) {
    throw createError({
      statusCode: 400,
      message: 'Name und E-Mail sind Pflichtfelder'
    })
  }

  // E-Mail-Format prüfen
  const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/
  if (!emailRegex.test(body.email)) {
    throw createError({
      statusCode: 400,
      message: 'Ungültiges E-Mail-Format'
    })
  }

  const kontakte = await leseKontakte()

  // Duplikat prüfen
  if (kontakte.some(k => k.email === body.email)) {
    throw createError({
      statusCode: 409,
      message: 'E-Mail-Adresse existiert bereits'
    })
  }

  const neuerKontakt: Kontakt = {
    id: naechsteId(kontakte),
    name: body.name,
    email: body.email,
    telefon: body.telefon || undefined,
    firma: body.firma || undefined,
    rolle: body.rolle || undefined
  }

  kontakte.push(neuerKontakt)
  await schreibeKontakte(kontakte)

  setResponseStatus(event, 201)
  return neuerKontakt
})
server/api/kontakte/[id].put.ts
export default defineEventHandler(async (event) => {
  const id = Number(getRouterParam(event, 'id'))
  const body = await readBody(event)

  if (isNaN(id)) {
    throw createError({ statusCode: 400, message: 'Ungültige ID' })
  }

  const kontakte = await leseKontakte()
  const index = kontakte.findIndex(k => k.id === id)

  if (index === -1) {
    throw createError({ statusCode: 404, message: 'Kontakt nicht gefunden' })
  }

  // Felder aktualisieren (nur übergebene Felder)
  kontakte[index] = {
    ...kontakte[index],
    ...body,
    id  // ID darf nicht überschrieben werden
  }

  await schreibeKontakte(kontakte)
  return kontakte[index]
})
server/api/kontakte/[id].delete.ts
export default defineEventHandler(async (event) => {
  const id = Number(getRouterParam(event, 'id'))

  if (isNaN(id)) {
    throw createError({ statusCode: 400, message: 'Ungültige ID' })
  }

  const kontakte = await leseKontakte()
  const index = kontakte.findIndex(k => k.id === id)

  if (index === -1) {
    throw createError({ statusCode: 404, message: 'Kontakt nicht gefunden' })
  }

  kontakte.splice(index, 1)
  await schreibeKontakte(kontakte)

  setResponseStatus(event, 204)
  return null
})

Fehlerbehandlung

server/api/kontakte/[id].get.ts
export default defineEventHandler(async (event) => {
  const id = Number(getRouterParam(event, 'id'))

  if (isNaN(id)) {
    throw createError({
      statusCode: 400,
      statusMessage: 'Bad Request',
      message: 'ID muss eine Zahl sein'
    })
  }

  const kontakte = await leseKontakte()
  const kontakt = kontakte.find(k => k.id === id)

  if (!kontakt) {
    throw createError({
      statusCode: 404,
      statusMessage: 'Not Found',
      message: \\\`Kontakt mit ID \${id} nicht gefunden\\\`
    })
  }

  return kontakt
})
⚠️

Fehler-Konventionen

  • 400 — Ungültige Eingabe (fehlende Felder, falsches Format)
  • 404 — Ressource nicht gefunden
  • 409 — Konflikt (z.B. Duplikat)
  • 500 — Interner Serverfehler (unbehandelte Ausnahme)

createError() erzeugt eine H3-konforme Fehlerantwort. Der Client erhält ein JSON-Objekt mit statusCode und message.

Endpunkte testen

Teste deine API mit curl, den Nuxt DevTools oder einem HTTP-Client wie Insomnia:

Terminal
# Alle Kontakte abrufen
curl http://localhost:3000/api/kontakte

# Kontakt suchen
curl 'http://localhost:3000/api/kontakte?suche=anna'

# Neuen Kontakt erstellen
curl -X POST http://localhost:3000/api/kontakte \\
  -H 'Content-Type: application/json' \\
  -d '{"name": "Lisa Müller", "email": "lisa@beispiel.de"}'

# Kontakt aktualisieren
curl -X PUT http://localhost:3000/api/kontakte/1 \\
  -H 'Content-Type: application/json' \\
  -d '{"telefon": "+49 172 5555555"}'

# Kontakt löschen
curl -X DELETE http://localhost:3000/api/kontakte/1

Zusammenfassung

💡

Server-API Konzepte

  • server/api/ — Verzeichnis für API-Routen
  • Dateiname → Methode.get.ts, .post.ts, etc.
  • [id] — Dynamische Parameter in Routen
  • readBody() — Request-Body lesen
  • getQuery() — Query-Parameter lesen
  • createError() — Strukturierte Fehlerantworten
  • server/utils/ — Geteilte Funktionen (Auto-Import)