nuxt3 server-routes nitro vue backend

Nuxt 3 Server Routes: Backend yazmadan backend yazma rehberi

Abdullah Bozdağ 20 Mart 2026
Nuxt 3 Server Routes: Backend yazmadan backend yazma rehberi

Neden ayrı bir backend kurmak istemiyorsun?

Küçük-orta ölçekli bir proje başlatıyorsun. Frontend Nuxt 3, veritabanı PostgreSQL ya da MongoDB. Arada bir Express ya da Fastify sunucusu kurmak, ayrı repo açmak, deploy pipeline'ı çoğaltmak istemiyorsun. İşte server routes tam bu noktada devreye giriyor.

Nuxt 3, Nitro engine üzerinde çalışan bir server katmanı sunuyor. server/api/ dizinine bir dosya atıyorsun, o dosya otomatik olarak bir API endpoint'i oluyor. Ayrı bir backend framework'ü yok, ayrı bir port yok. Build aldığında Nitro bunu Node.js sunucusu, Cloudflare Worker, Vercel Edge Function ya da Deno olarak deploy edebiliyor.

Biz bunu birkaç dahili projede kullandık. Basit CRUD API'leri, webhook handler'ları ve üçüncü parti servis proxy'leri için gerçekten işe yarıyor. Ama her şey için doğru araç değil. Ona da geleceğim.

İlk endpoint'ini oluştur

Proje kökünde server/api/ dizini oluştur. İçine bir dosya at:

// server/api/hello.get.ts
export default defineEventHandler((event) => {
return {
message: 'Merhaba dünya',
timestamp: new Date().toISOString()
}
})

Bu kadar. /api/hello adresine GET isteği attığında JSON dönecek. Dosya adındaki .get kısmı HTTP method'unu belirtiyor. .post, .put, .delete de kullanabilirsin. Method belirtmezsen tüm HTTP method'larına yanıt verir.

Dikkat: defineEventHandler Nitro'nun (h3 kütüphanesinin) fonksiyonu. Express'teki req, res yerine tek bir event objesi alıyorsun. Response'u return etmen yeterli, res.json() çağırmana gerek yok.

Request body ve query parametreleri

POST endpoint'inde body okumak için readBody, query string için getQuery kullanıyorsun:

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

if (!body.email || !body.name) {
throw createError({
statusCode: 400,
statusMessage: 'name ve email zorunlu'
})
}

// Burada veritabanına kayıt yaparsın
return { success: true, user: { name: body.name, email: body.email } }
})
// server/api/users.get.ts
export default defineEventHandler((event) => {
const query = getQuery(event)
const page = Number(query.page) || 1
const limit = Number(query.limit) || 20

// Veritabanından çek
return { page, limit, users: [] }
})

createError ile hata fırlattığında Nuxt bunu uygun HTTP status code'uyla JSON olarak döndürüyor. Try-catch bloklarıyla uğraşmana gerek yok, ama production'da global bir error handler eklemeyi unutma.

Dinamik route'lar ve iç içe dizinler

Dosya tabanlı routing, Nuxt'ın page'lerindeki mantıkla aynı çalışıyor. Köşeli parantezle parametre tanımlıyorsun:

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

if (!id) {
throw createError({ statusCode: 400, statusMessage: 'id parametresi gerekli' })
}

// Veritabanından id'ye göre çek
return { id, name: 'Test User' }
})

Bu dosya /api/users/123 gibi isteklere yanıt verir. Catch-all route için [...slug].ts kullanabilirsin ama genelde buna ihtiyaç duymazsın.

Dizin yapısını REST convention'a göre düzenlemeni öneririm:

server/
api/
users/
index.get.ts → GET /api/users
index.post.ts → POST /api/users
[id].get.ts → GET /api/users/:id
[id].put.ts → PUT /api/users/:id
[id].delete.ts → DELETE /api/users/:id
auth/
login.post.ts → POST /api/auth/login
logout.post.ts → POST /api/auth/logout

Middleware ve paylaşılan mantık

Her endpoint'te auth kontrolü tekrarlamak istemezsin. server/middleware/ dizinindeki dosyalar tüm server isteklerinde çalışır:

// server/middleware/auth.ts
export default defineEventHandler((event) => {
const url = getRequestURL(event)

// Public endpoint'leri atla
if (url.pathname.startsWith('/api/auth/')) return

const token = getHeader(event, 'authorization')?.replace('Bearer ', '')

if (!token) {
throw createError({ statusCode: 401, statusMessage: 'Yetkisiz erişim' })
}

// Token doğrulama mantığın
// event.context.user = decodedUser şeklinde sonraki handler'lara veri aktarabilirsin
})

Burada bir sorun var: middleware tüm route'lara uygulanıyor. Seçici olman gerekiyor, yoksa public endpoint'lerin de auth ister. URL kontrolü biraz kaba bir yöntem ama Nuxt 3'te route-level middleware tanımlama şu an bu şekilde çalışıyor. Alternatif olarak ortak bir utils fonksiyonu yazıp sadece ihtiyacın olan handler'larda çağırabilirsin.

// server/utils/auth.ts
export async function requireAuth(event: any) {
const token = getHeader(event, 'authorization')?.replace('Bearer ', '')
if (!token) {
throw createError({ statusCode: 401, statusMessage: 'Token gerekli' })
}
// Doğrula ve user döndür
return { id: '1', email: '[email protected]' }
}

// server/api/protected-route.get.ts
export default defineEventHandler(async (event) => {
const user = await requireAuth(event)
return { message: `Merhaba ${user.email}` }
})

server/utils/ dizinindeki dosyalar Nitro tarafından otomatik import ediliyor. Ayrıca import yazman gerekmez.

Veritabanı bağlantısı: gerçek dünyada nasıl?

Biz Prisma ile kullandık. Bağlantıyı her istekte yeniden oluşturmamak için bir singleton pattern gerekiyor:

// server/utils/db.ts
import { PrismaClient } from '@prisma/client'

let prisma: PrismaClient

export function useDB() {
if (!prisma) {
prisma = new PrismaClient()
}
return prisma
}

// server/api/posts.get.ts
export default defineEventHandler(async () => {
const db = useDB()
const posts = await db.post.findMany({
take: 20,
orderBy: { createdAt: 'desc' }
})
return posts
})

Drizzle ORM de iyi çalışıyor ve tip güvenliği konusunda bir adım önde. Hangi ORM'i seçersen seç, connection pooling ayarını production'da mutlaka yap. Serverless ortamda (Vercel, Cloudflare) deploy ediyorsan connection limit sorunuyla karşılaşabilirsin.

Ne zaman yeterli değil?

Server routes ile bir noktaya kadar gidebilirsin. Ama şu durumlarda ayrı bir backend düşünmeye başla:

  1. WebSocket desteği lazımsa: Nitro'da deneysel WebSocket desteği var ama production-ready değil. Gerçek zamanlı iletişim gerekiyorsa Socket.io veya ayrı bir servis daha sağlam.
  2. Karmaşık iş mantığı büyüyorsa: 20-30 endpoint'i geçtikten sonra dosya tabanlı routing karmaşıklaşıyor. Service layer, repository pattern gibi yapılar kurmak istiyorsan NestJS ya da AdonisJS gibi bir framework daha uygun.
  3. Farklı takımlar frontend ve backend'i ayrı geliştiriyorsa: Monorepo'da bile olsan, ayrı deploy cycle'ları istiyorsan ayrı servisler daha mantıklı.
  4. CPU-yoğun işlemler varsa: Resim işleme, PDF oluşturma gibi ağır işler Nitro'nun event loop'unu bloklar. Bunları bir queue sistemiyle ayrı bir worker'a taşıman gerekir.

Küçük-orta ölçekli projelerde, dahili araçlarda veya MVP aşamasında server routes fazlasıyla yeterli. 5-6 endpoint'li bir admin paneli için Express sunucusu ayağa kaldırmak overkill.

Runtime config ve environment değişkenleri

API anahtarlarını ve hassas bilgileri .env dosyasında tutarsın. Server route'larından erişmek için:

// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
apiSecret: '', // Sadece server tarafında erişilebilir
databaseUrl: '', // Sadece server tarafında
public: {
apiBase: '/api' // Client tarafından da erişilebilir
}
}
})

// .env
NUXT_API_SECRET=gizli-anahtar-123
NUXT_DATABASE_URL=postgresql://localhost:5432/mydb
// server/api/external.get.ts
export default defineEventHandler((event) => {
const config = useRuntimeConfig(event)

// config.apiSecret → sadece server'da
// config.public.apiBase → her yerde

return $fetch('https://external-api.com/data', {
headers: { 'X-API-Key': config.apiSecret }
})
})

Nuxt, NUXT_ prefix'iyle başlayan env değişkenlerini otomatik olarak runtimeConfig'e eşliyor. NUXT_API_SECRET yazdığında runtimeConfig.apiSecret olarak erişebilirsin. Tip güvenliği istiyorsan runtimeConfig için bir interface tanımla.

Sırada ne var?

Server routes'u production'da kullanacaksan birkaç şeyi es geçme: rate limiting (bir middleware ile basitçe yapılır), request validation (zod veya valibot ile şema tanımla), ve error logging (Sentry veya benzeri bir servise bağla). Nitro'nun server/plugins/ dizini uygulama başlarken çalışacak kodlar için iyi bir yer. Veritabanı migration'ları, cron job tanımları gibi şeyleri oraya koyabilirsin.

Resmi dökümandaki server directory sayfasını yer işaretine ekle. Nitro'nun kendi dökümantasyonu da ayrıca okunmaya değer çünkü Nuxt'ın server tarafı aslında tamamen Nitro.

Paylas:

Abdullah Bozdağ

Abdullah Bozdağ

RadKod Ekibi