Server-Routes (API)
Baue Backend-Endpunkte direkt in deiner Nuxt-App — wie Rails API-Controller, aber ohne den ganzen Boilerplate.
API-Endpunkte mit Nitro
In Rails hast du Controller, die HTTP-Requests verarbeiten. In Nuxt erstellst du Dateien in server/api/ — jede Datei wird automatisch zu einem API-Endpunkt. Kein Router, kein Controller-Setup.
// server/api/hello.ts
// → GET /api/hello
export default defineEventHandler(() => {
return {
message: 'Hallo Welt! 👋',
timestamp: new Date().toISOString()
}
})
// Das ist alles! Kein Router, kein Controller.
// Die Datei IST der Endpunkt.
// Rails-Äquivalent:
// # config/routes.rb
// get '/api/hello', to: 'api/hello#index'
//
// # app/controllers/api/hello_controller.rb
// class Api::HelloController < ApplicationController
// def index
// render json: { message: 'Hallo Welt!' }
// end
// endRails-Vergleich: API-Controller
Rails: Route in routes.rb → Controller-Klasse → Action-Methode → JSON-Response
Nuxt: Datei in server/api/ → fertig!
Eine Datei ersetzt Route-Definition + Controller + Action. Das ist Convention over Configuration auf einem neuen Level.
HTTP-Methoden: GET, POST, PUT, DELETE
Nuxt unterstützt zwei Wege, HTTP-Methoden zu definieren: Über den Dateinamen oder innerhalb des Handlers.
Methode 1: Dateiname
server/api/
├── posts/
│ ├── index.get.ts → GET /api/posts (Liste)
│ ├── index.post.ts → POST /api/posts (Erstellen)
│ ├── [id].get.ts → GET /api/posts/:id (Lesen)
│ ├── [id].put.ts → PUT /api/posts/:id (Aktualisieren)
│ └── [id].delete.ts → DELETE /api/posts/:id (Löschen)
# Rails-Äquivalent:
# resources :posts → 7 Routen in einem Befehl
# In Nuxt: 5 Dateien, aber null Konfiguration nötigMethode 2: Im Handler prüfen
// server/api/posts/[id].ts
// Alle Methoden für /api/posts/:id in einer Datei
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, 'id')
const method = event.method
// GET /api/posts/:id
if (method === 'GET') {
const post = await findPost(id)
if (!post) {
throw createError({ statusCode: 404, message: 'Post nicht gefunden' })
}
return post
}
// PUT /api/posts/:id
if (method === 'PUT') {
const body = await readBody(event)
return await updatePost(id, body)
}
// DELETE /api/posts/:id
if (method === 'DELETE') {
await deletePost(id)
return { deleted: true }
}
// Methode nicht erlaubt
throw createError({ statusCode: 405, message: 'Methode nicht erlaubt' })
})Welche Methode wählen?
Separate Dateien (Methode 1) sind übersichtlicher und empfohlen für einfache CRUD-Operationen. Ein Handler (Methode 2) ist besser, wenn GET/PUT/DELETE auf derselben Ressource gemeinsame Logik teilen.
Query-Parameter & Request Body
Nuxt bietet Hilfsfunktionen zum Lesen von URL-Parametern, Query-Strings und dem Request-Body — ähnlich wie params in Rails.
// server/api/posts/index.get.ts
export default defineEventHandler(async (event) => {
// URL-Parameter: /api/posts/:id
const id = getRouterParam(event, 'id')
// Query-String: /api/posts?page=2&limit=10
const { page = '1', limit = '10', search } = getQuery(event)
// Request-Body (POST/PUT)
const body = await readBody(event)
// body = { title: '...', content: '...' }
// Headers lesen
const authToken = getHeader(event, 'authorization')
// Cookies lesen
const sessionId = getCookie(event, 'session_id')
// Rails-Vergleich:
// params[:id] → getRouterParam(event, 'id')
// params[:page] → getQuery(event).page
// params[:title] → (await readBody(event)).title
// request.headers['Authorization'] → getHeader(event, 'authorization')
// cookies[:session_id] → getCookie(event, 'session_id')
return { id, page, limit, search }
})Rails-Vergleich: params
In Rails greifst du auf alles über params zu — URL-Segmente, Query-String, Body. In Nuxt sind die Quellen getrennt: getRouterParam() für URL-Segmente, getQuery() für Query-Strings und readBody() für den POST-Body. Expliziter, aber klarer.
Error Handling mit createError
Für HTTP-Fehler nutzt du createError() — wie raise ActiveRecord::RecordNotFound in Rails, aber mit explizitem Status-Code.
// server/api/posts/[id].get.ts
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, 'id')
// ID validieren
if (!id || isNaN(Number(id))) {
throw createError({
statusCode: 400,
statusMessage: 'Ungültige ID'
})
}
// Post suchen (simuliert)
const post = await findPostById(Number(id))
// 404 wenn nicht gefunden
if (!post) {
throw createError({
statusCode: 404,
statusMessage: 'Post nicht gefunden'
})
}
return post
})
// Rails-Vergleich:
// def show
// @post = Post.find(params[:id])
// rescue ActiveRecord::RecordNotFound
// render json: { error: 'Not found' }, status: :not_found
// endValidierung
Für Request-Validierung empfiehlt sich die Library zod. Sie validiert und typisiert gleichzeitig — wie Strong Parameters in Rails, aber mit vollständiger Typ-Sicherheit.
Server Middleware
Server-Middleware läuft vor jedem API-Request — wie Rack Middleware in Rails. Perfekt für Logging, Auth-Checks oder CORS-Header.
// server/middleware/log.ts
// Läuft bei JEDEM Server-Request — wie Rack::Logger
export default defineEventHandler((event) => {
const method = event.method
const url = getRequestURL(event).pathname
console.log(`[${new Date().toISOString()}] ${method} ${url}`)
// Kein return → Request geht weiter zum eigentlichen Handler
})// server/middleware/auth.ts
// API-Authentifizierung — wie before_action :authenticate!
export default defineEventHandler(async (event) => {
// Nur API-Routen schützen
const url = getRequestURL(event).pathname
if (!url.startsWith('/api/admin')) return
const token = getHeader(event, 'authorization')?.replace('Bearer ', '')
if (!token) {
throw createError({
statusCode: 401,
statusMessage: 'Nicht authentifiziert'
})
}
// Token verifizieren (z.B. mit jose oder jsonwebtoken)
try {
const user = await verifyToken(token)
// User am Event speichern — wie current_user in Rails
event.context.user = user
} catch {
throw createError({
statusCode: 403,
statusMessage: 'Ungültiges Token'
})
}
})Server- vs. Route-Middleware
Server-Middleware (server/middleware/) läuft auf dem Server bei jedem HTTP-Request — wie Rack Middleware.
Route-Middleware (middleware/) läuft im Browser bei clientseitiger Navigation — wie before_action. Verwechsle die beiden nicht!
Datenbank-Integration
Nuxt hat kein eingebautes ORM wie Rails ActiveRecord. Du wählst selbst — die beliebtesten Optionen:
| ORM | Stil | Rails-Äquivalent |
|---|---|---|
| Prisma | Schema-first, Migrations | Am nächsten an ActiveRecord |
| Drizzle | TypeScript-first, leichtgewichtig | Wie Sequel in Ruby |
| Nuxt Hub / D1 | Serverless SQLite | SQLite mit Rails |
// server/utils/db.ts
// Datenbank-Verbindung als Server-Utility
// Wird automatisch in allen server/-Dateien verfügbar
import { PrismaClient } from '@prisma/client'
// Singleton-Pattern (wie in Rails mit ActiveRecord::Base.connection)
let prisma: PrismaClient
export function usePrisma() {
if (!prisma) {
prisma = new PrismaClient()
}
return prisma
}
// Verwendung in API-Routen:
// const db = usePrisma()
// const posts = await db.post.findMany()Komplettes CRUD-Beispiel
Hier ist ein vollständiges CRUD-API für Posts — das Nuxt-Äquivalent zu einem Rails PostsController mit allen Standardaktionen:
// server/api/posts/index.get.ts — GET /api/posts
// Rails: PostsController#index
export default defineEventHandler(async (event) => {
const { page = '1', limit = '10' } = getQuery(event)
const db = usePrisma()
const posts = await db.post.findMany({
skip: (Number(page) - 1) * Number(limit),
take: Number(limit),
orderBy: { createdAt: 'desc' },
select: { id: true, title: true, slug: true, excerpt: true, createdAt: true }
})
const total = await db.post.count()
return {
posts,
pagination: {
page: Number(page),
limit: Number(limit),
total,
pages: Math.ceil(total / Number(limit))
}
}
})// server/api/posts/index.post.ts — POST /api/posts
// Rails: PostsController#create
export default defineEventHandler(async (event) => {
const body = await readBody(event)
// Validierung (wie Strong Parameters)
if (!body.title || !body.content) {
throw createError({
statusCode: 422,
statusMessage: 'Titel und Inhalt sind erforderlich'
})
}
const db = usePrisma()
const post = await db.post.create({
data: {
title: body.title,
content: body.content,
slug: body.title.toLowerCase().replace(/\s+/g, '-'),
excerpt: body.content.substring(0, 200),
authorId: event.context.user?.id // Aus Auth-Middleware
}
})
// 201 Created zurückgeben
setResponseStatus(event, 201)
return post
})// server/api/posts/[id].get.ts — GET /api/posts/:id
// Rails: PostsController#show
export default defineEventHandler(async (event) => {
const id = Number(getRouterParam(event, 'id'))
const db = usePrisma()
const post = await db.post.findUnique({
where: { id },
include: {
author: { select: { name: true, avatar: true } },
comments: { orderBy: { createdAt: 'desc' }, take: 20 }
}
})
if (!post) {
throw createError({ statusCode: 404, statusMessage: 'Post nicht gefunden' })
}
return post
})// server/api/posts/[id].put.ts — PUT /api/posts/:id
// Rails: PostsController#update
export default defineEventHandler(async (event) => {
const id = Number(getRouterParam(event, 'id'))
const body = await readBody(event)
const db = usePrisma()
// Prüfen ob Post existiert
const existing = await db.post.findUnique({ where: { id } })
if (!existing) {
throw createError({ statusCode: 404, statusMessage: 'Post nicht gefunden' })
}
// Nur erlaubte Felder aktualisieren (wie permit in Rails)
const post = await db.post.update({
where: { id },
data: {
title: body.title ?? existing.title,
content: body.content ?? existing.content,
slug: body.title
? body.title.toLowerCase().replace(/\s+/g, '-')
: existing.slug,
updatedAt: new Date()
}
})
return post
})// server/api/posts/[id].delete.ts — DELETE /api/posts/:id
// Rails: PostsController#destroy
export default defineEventHandler(async (event) => {
const id = Number(getRouterParam(event, 'id'))
const db = usePrisma()
// Prüfen ob Post existiert
const existing = await db.post.findUnique({ where: { id } })
if (!existing) {
throw createError({ statusCode: 404, statusMessage: 'Post nicht gefunden' })
}
await db.post.delete({ where: { id } })
// 204 No Content (wie in Rails: head :no_content)
setResponseStatus(event, 204)
return null
})Rails CRUD zum Vergleich
In Rails wäre das alles in einer Datei: app/controllers/posts_controller.rb mit index, show, create, update, destroy. In Nuxt ist es auf mehrere Dateien verteilt — jede Datei ist dafür kleiner und fokussierter. Beides hat Vor- und Nachteile.
Die komplette API-Struktur
So sieht eine typische Server-Struktur für eine Nuxt-App aus:
server/
├── api/ # API-Endpunkte
│ ├── posts/
│ │ ├── index.get.ts # GET /api/posts
│ │ ├── index.post.ts # POST /api/posts
│ │ ├── [id].get.ts # GET /api/posts/:id
│ │ ├── [id].put.ts # PUT /api/posts/:id
│ │ └── [id].delete.ts # DELETE /api/posts/:id
│ ├── auth/
│ │ ├── login.post.ts # POST /api/auth/login
│ │ ├── logout.post.ts # POST /api/auth/logout
│ │ └── me.get.ts # GET /api/auth/me
│ └── health.get.ts # GET /api/health
├── middleware/ # Server-Middleware (jeder Request)
│ ├── log.ts # Request-Logging
│ └── auth.ts # Token-Validierung
├── utils/ # Server-Utilities (auto-importiert)
│ ├── db.ts # Datenbank-Verbindung
│ └── auth.ts # Auth-Hilfsfunktionen
└── plugins/ # Server-Plugins (Startup)
└── migrations.ts # DB-Migrations beim Start
# Rails-Äquivalent:
# app/controllers/api/ → server/api/
# config/initializers/ → server/plugins/
# app/middleware/ → server/middleware/
# lib/ → server/utils/Geschafft!
Du kennst jetzt die Grundlagen von Nuxt: Projekt-Struktur, Routing, Layouts, Data Fetching und Server-Routes. Mit diesen Bausteinen kannst du bereits vollständige Webanwendungen bauen — Frontend und Backend in einem Projekt.