Hasura ile Next.js Uygulama Geliştirme: GraphQL API Entegrasyonu
Modern web geliştirme dünyasında frontend ve backend arasındaki köprüyü kurmak her zaman zahmetli olmuştur. REST API yazıyorsun, endpoint’leri tanımlıyorsun, documentation hazırlıyorsun, sonra frontend tarafında her şeyi tekrar implement ediyorsun. Hasura bu döngüyü kırıyor. PostgreSQL veritabanının üzerine otomatik olarak GraphQL API üretiyor ve Next.js ile birleştiğinde gerçekten güçlü bir stack ortaya çıkıyor. Bu yazıda sıfırdan production-ready bir uygulama nasıl kurulur, onu konuşacağız.
Hasura Nedir ve Neden Kullanmalıyız
Hasura, PostgreSQL veritabanına bağlanıp anında GraphQL API üreten bir engine. Tablolarını tanımlıyorsun, ilişkileri kuruyorsun, izinleri ayarlıyorsun ve GraphQL API’n hazır. Ama işin güzel tarafı sadece bu değil. Real-time subscription desteği var, custom business logic için action ve event trigger sistemi var, remote schema ile başka GraphQL servisleri entegre edebiliyorsun.
Next.js tarafında ise App Router ile birlikte server component’ler, server action’lar ve built-in API routes ile tam stack bir uygulama yapısı sunuyor. Bu iki teknolojiyi birleştirdiğinde hem development hızın artıyor hem de production’da scale etmesi kolay bir mimari elde ediyorsun.
Ortam Kurulumu
Docker Compose ile başlayalım. Gerçek dünyada genellikle bunu local development için kullanıyoruz, production’da managed PostgreSQL ve Hasura Cloud tercih ediliyor.
mkdir hasura-nextjs-app && cd hasura-nextjs-app
mkdir hasura && touch hasura/docker-compose.yml
# docker-compose.yml
version: '3.6'
services:
postgres:
image: postgres:15
restart: always
volumes:
- db_data:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: postgrespassword
POSTGRES_DB: myapp
ports:
- "5432:5432"
graphql-engine:
image: hasura/graphql-engine:v2.36.0
ports:
- "8080:8080"
restart: always
environment:
HASURA_GRAPHQL_METADATA_DATABASE_URL: postgres://postgres:postgrespassword@postgres:5432/myapp
PG_DATABASE_URL: postgres://postgres:postgrespassword@postgres:5432/myapp
HASURA_GRAPHQL_ENABLE_CONSOLE: "true"
HASURA_GRAPHQL_DEV_MODE: "true"
HASURA_GRAPHQL_ENABLED_LOG_TYPES: startup, http-log, webhook-log, websocket-log, query-log
HASURA_GRAPHQL_ADMIN_SECRET: myadminsecretkey
HASURA_GRAPHQL_JWT_SECRET: '{"type":"HS256","key":"your-super-secret-jwt-key-min-32-chars"}'
depends_on:
- postgres
volumes:
db_data:
docker-compose up -d
# Logları kontrol et
docker-compose logs -f graphql-engine
Hasura Console’a http://localhost:8080/console adresinden ulaşabilirsin. Admin secret olarak myadminsecretkey kullanıyoruz.
Hasura CLI Kurulumu ve Migration Yönetimi
Production ortamında schema değişikliklerini migration dosyaları ile yönetmek kritik. Hasura CLI bu konuda çok işe yarıyor.
# Hasura CLI kurulumu
curl -L https://github.com/hasura/graphql-engine/raw/stable/cli/get.sh | bash
# Hasura projesini initialize et
hasura init hasura-project --endpoint http://localhost:8080 --admin-secret myadminsecretkey
cd hasura-project
# Migration oluştur
hasura migrate create "init_schema" --from-server
# Mevcut durumu server'a uygula
hasura migrate apply
# Metadata export
hasura metadata export
Şimdi gerçek bir senaryo üzerinden gidelim. E-ticaret benzeri bir uygulama yapıyoruz. Kullanıcılar, ürünler ve siparişler var.
# Yeni migration oluştur
hasura migrate create "create_products_and_orders" --database-name default
# migrations/xxxxxxx_create_products_and_orders/up.sql
cat > migrations/default/xxxxxxx_create_products_and_orders/up.sql << 'EOF'
CREATE TABLE users (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'user',
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE products (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
name TEXT NOT NULL,
description TEXT,
price NUMERIC(10,2) NOT NULL,
stock_count INTEGER NOT NULL DEFAULT 0,
created_by UUID REFERENCES users(id),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE orders (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
user_id UUID NOT NULL REFERENCES users(id),
status TEXT NOT NULL DEFAULT 'pending',
total_amount NUMERIC(10,2) NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE order_items (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
order_id UUID NOT NULL REFERENCES orders(id),
product_id UUID NOT NULL REFERENCES products(id),
quantity INTEGER NOT NULL,
unit_price NUMERIC(10,2) NOT NULL
);
EOF
hasura migrate apply --database-name default
Hasura İzin Sistemi
Hasura’nın row-level security sistemi production’da en kritik parça. Her rol için hangi satırlara, hangi kolonlara erişilebileceğini tanımlıyorsun. Metadata dosyalarında bu şöyle görünüyor:
# metadata/databases/default/tables/public_orders.yaml
table:
name: orders
schema: public
select_permissions:
- role: user
permission:
columns:
- id
- status
- total_amount
- created_at
filter:
user_id:
_eq: X-Hasura-User-Id
allow_aggregations: false
- role: admin
permission:
columns: '*'
filter: {}
allow_aggregations: true
insert_permissions:
- role: user
permission:
columns:
- user_id
- status
check:
user_id:
_eq: X-Hasura-User-Id
set:
user_id: X-Hasura-User-Id
Bu yapı sayesinde bir kullanıcı sadece kendi siparişlerini görebiliyor. X-Hasura-User-Id JWT token’dan otomatik olarak alınıyor.
Next.js Projesi Kurulumu
npx create-next-app@latest frontend --typescript --tailwind --app
cd frontend
# GraphQL client olarak urql kullanacağız, Apollo da tercih edilebilir
npm install @urql/next @urql/core graphql
npm install -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typescript-urql
GraphQL Code Generator konfigürasyonu:
// codegen.ts
import type { CodegenConfig } from '@graphql-codegen/cli';
const config: CodegenConfig = {
overwrite: true,
schema: {
'http://localhost:8080/v1/graphql': {
headers: {
'x-hasura-admin-secret': 'myadminsecretkey',
'x-hasura-role': 'user',
},
},
},
documents: 'src/**/*.graphql',
generates: {
'src/generated/graphql.ts': {
plugins: [
'typescript',
'typescript-operations',
'typescript-urql',
],
config: {
withHooks: true,
urqlImportFrom: '@urql/next',
},
},
},
};
export default config;
URQL client kurulumu ve server/client component ayrımı:
// src/lib/urql.ts
import { createClient, cacheExchange, fetchExchange, ssrExchange } from '@urql/core';
const isServerSide = typeof window === 'undefined';
export function makeClient(token?: string) {
const ssr = ssrExchange({
isClient: !isServerSide,
});
return createClient({
url: process.env.NEXT_PUBLIC_HASURA_URL || 'http://localhost:8080/v1/graphql',
exchanges: [cacheExchange, ssr, fetchExchange],
fetchOptions: () => {
const headers: Record<string, string> = {
'content-type': 'application/json',
};
if (token) {
headers['authorization'] = `Bearer ${token}`;
}
return { headers };
},
});
}
// Server component için client
export function getServerClient(token?: string) {
return makeClient(token);
}
GraphQL Query’leri ve Kod Üretimi
Şimdi gerçek query’leri yazalım:
# src/queries/products.graphql
query GetProducts($limit: Int = 10, $offset: Int = 0) {
products(limit: $limit, offset: $offset, order_by: {created_at: desc}) {
id
name
description
price
stock_count
created_at
}
products_aggregate {
aggregate {
count
}
}
}
mutation CreateOrder($items: [order_items_insert_input!]!, $total: numeric!) {
insert_orders_one(object: {
status: "pending",
total_amount: $total,
order_items: {
data: $items
}
}) {
id
status
total_amount
created_at
}
}
subscription OrderStatusUpdated($orderId: uuid!) {
orders_by_pk(id: $orderId) {
id
status
updated_at
}
}
# Kod üret
npx graphql-codegen --config codegen.ts
Server Component ile Veri Çekme
Next.js App Router’da server component’lerde direkt GraphQL sorgusu çalıştırabiliyorsun. Bu sayede hydration sorunları yaşamıyorsun ve SEO açısından avantajlı oluyor.
// src/app/products/page.tsx
import { getServerClient } from '@/lib/urql';
import { GetProductsDocument } from '@/generated/graphql';
import { cookies } from 'next/headers';
import ProductGrid from '@/components/ProductGrid';
async function getProducts(page: number = 1) {
const limit = 12;
const offset = (page - 1) * limit;
// Server-side auth token alma
const cookieStore = cookies();
const token = cookieStore.get('auth-token')?.value;
const client = getServerClient(token);
const result = await client.query(GetProductsDocument, {
limit,
offset,
});
if (result.error) {
console.error('GraphQL Error:', result.error);
throw new Error('Ürünler yüklenirken hata oluştu');
}
return {
products: result.data?.products || [],
total: result.data?.products_aggregate.aggregate?.count || 0,
};
}
export default async function ProductsPage({
searchParams,
}: {
searchParams: { page?: string };
}) {
const page = Number(searchParams.page) || 1;
const { products, total } = await getProducts(page);
return (
<div className="container mx-auto py-8">
<h1 className="text-2xl font-bold mb-6">Ürünler</h1>
<ProductGrid products={products} />
<div className="mt-8 text-sm text-gray-500">
Toplam {total} ürün
</div>
</div>
);
}
Hasura Actions ile Custom Business Logic
Bazı işlemler direkt database mutation ile yapılamaz. Ödeme işlemi, email gönderme, external API çağrısı gibi senaryolar için Hasura Actions kullanıyoruz. Action’ı Console’dan ya da metadata ile tanımlıyorsun, Hasura bunun için webhook endpoint’i çağırıyor.
// src/app/api/hasura/actions/process-payment/route.ts
import { NextRequest, NextResponse } from 'next/server';
interface PaymentInput {
order_id: string;
payment_method: string;
amount: number;
}
export async function POST(req: NextRequest) {
// Hasura action secret doğrulaması
const actionSecret = req.headers.get('x-hasura-action-secret');
if (actionSecret !== process.env.HASURA_ACTION_SECRET) {
return NextResponse.json(
{ message: 'Unauthorized' },
{ status: 401 }
);
}
const body = await req.json();
const { input } = body as { input: { payment: PaymentInput } };
const { order_id, payment_method, amount } = input.payment;
try {
// Ödeme gateway entegrasyonu (Stripe, iyzico vb.)
const paymentResult = await processPaymentGateway({
orderId: order_id,
method: payment_method,
amount,
});
if (paymentResult.success) {
// Hasura üzerinden order status güncelle
await updateOrderStatus(order_id, 'paid', paymentResult.transaction_id);
return NextResponse.json({
success: true,
transaction_id: paymentResult.transaction_id,
message: 'Ödeme başarıyla tamamlandı',
});
}
return NextResponse.json({
success: false,
transaction_id: null,
message: 'Ödeme işlemi başarısız',
});
} catch (error) {
// Hasura action hataları bu formatta dönmeli
return NextResponse.json(
{
message: 'Ödeme işlenirken hata oluştu',
extensions: {
code: 'PAYMENT_PROCESSING_ERROR',
},
},
{ status: 400 }
);
}
}
async function updateOrderStatus(
orderId: string,
status: string,
transactionId: string
) {
const response = await fetch(
`${process.env.HASURA_ENDPOINT}/v1/graphql`,
{
method: 'POST',
headers: {
'content-type': 'application/json',
'x-hasura-admin-secret': process.env.HASURA_ADMIN_SECRET!,
},
body: JSON.stringify({
query: `
mutation UpdateOrder($orderId: uuid!, $status: String!, $txId: String!) {
update_orders_by_pk(
pk_columns: { id: $orderId }
_set: { status: $status, transaction_id: $txId }
) {
id
status
}
}
`,
variables: {
orderId,
status,
txId: transactionId,
},
}),
}
);
return response.json();
}
Event Trigger ile Asenkron İşlemler
Hasura Event Trigger’lar, veritabanında bir değişiklik olduğunda otomatik olarak webhook çağırıyor. Sipariş oluşturulduğunda email göndermek bunun klasik kullanım senaryosu.
// src/app/api/hasura/events/order-created/route.ts
import { NextRequest, NextResponse } from 'next/server';
interface HasuraEvent {
event: {
op: 'INSERT' | 'UPDATE' | 'DELETE';
data: {
old: Record<string, unknown> | null;
new: Record<string, unknown> | null;
};
};
table: {
schema: string;
name: string;
};
}
export async function POST(req: NextRequest) {
const triggerSecret = req.headers.get('x-hasura-event-secret');
if (triggerSecret !== process.env.HASURA_EVENT_SECRET) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const payload = await req.json() as HasuraEvent;
if (payload.event.op === 'INSERT' && payload.event.data.new) {
const order = payload.event.data.new;
// Kullanıcı bilgisini çek ve email gönder
await sendOrderConfirmationEmail({
orderId: order.id as string,
userId: order.user_id as string,
totalAmount: order.total_amount as number,
});
}
// Event trigger'lara her zaman 200 dönmek lazım
// Yoksa Hasura retry mekanizması devreye giriyor
return NextResponse.json({ success: true });
}
async function sendOrderConfirmationEmail(params: {
orderId: string;
userId: string;
totalAmount: number;
}) {
// Email servis entegrasyonu (Resend, SendGrid vb.)
console.log(`Sipariş onay emaili gönderiliyor: ${params.orderId}`);
}
Real-time Subscription ile Sipariş Takibi
Hasura’nın en güçlü özelliklerinden biri WebSocket üzerinden çalışan subscription’lar. Client component’te sipariş durumunu gerçek zamanlı takip edebiliyorsun.
// src/components/OrderTracker.tsx
'use client';
import { useSubscription } from '@urql/next';
import { OrderStatusUpdatedDocument } from '@/generated/graphql';
const statusMessages: Record<string, string> = {
pending: 'Sipariş alındı, onay bekleniyor',
paid: 'Ödeme onaylandı',
processing: 'Hazırlanıyor',
shipped: 'Kargoya verildi',
delivered: 'Teslim edildi',
cancelled: 'İptal edildi',
};
const statusColors: Record<string, string> = {
pending: 'bg-yellow-100 text-yellow-800',
paid: 'bg-blue-100 text-blue-800',
processing: 'bg-purple-100 text-purple-800',
shipped: 'bg-orange-100 text-orange-800',
delivered: 'bg-green-100 text-green-800',
cancelled: 'bg-red-100 text-red-800',
};
export default function OrderTracker({ orderId }: { orderId: string }) {
const [result] = useSubscription({
query: OrderStatusUpdatedDocument,
variables: { orderId },
});
if (result.fetching) {
return <div className="animate-pulse h-8 bg-gray-200 rounded" />;
}
if (result.error) {
return (
<div className="text-red-500 text-sm">
Bağlantı hatası: {result.error.message}
</div>
);
}
const order = result.data?.orders_by_pk;
if (!order) return null;
const colorClass = statusColors[order.status] || 'bg-gray-100 text-gray-800';
const message = statusMessages[order.status] || order.status;
return (
<div className={`inline-flex items-center px-3 py-1 rounded-full text-sm font-medium ${colorClass}`}>
<span className="w-2 h-2 rounded-full bg-current mr-2 animate-pulse" />
{message}
</div>
);
}
Environment Yönetimi ve Güvenlik
Production’a geçmeden önce environment variable’ları doğru ayarlamak şart.
# .env.local (Next.js)
NEXT_PUBLIC_HASURA_URL=https://your-hasura-app.hasura.app/v1/graphql
NEXT_PUBLIC_HASURA_WS_URL=wss://your-hasura-app.hasura.app/v1/graphql
HASURA_ADMIN_SECRET=production-admin-secret
HASURA_ENDPOINT=https://your-hasura-app.hasura.app
HASURA_ACTION_SECRET=action-webhook-secret
HASURA_EVENT_SECRET=event-webhook-secret
JWT_SECRET=your-super-secret-jwt-key-minimum-32-characters
NEXTAUTH_SECRET=nextauth-secret
NEXTAUTH_URL=https://yourdomain.com
Production’da dikkat edilmesi gerekenler:
- HASURA_GRAPHQL_ENABLE_CONSOLE production’da
falseolmalı - Admin secret environment variable’dan gelmeli, asla kodda hardcoded olmamalı
- JWT secret en az 32 karakter uzunluğunda olmalı
- CORS ayarları sadece kendi domain’ini izin verecek şekilde yapılandırılmalı
- Rate limiting için Hasura’nın önüne bir reverse proxy (nginx/caddy) koyulmalı
- Introspection production’da kapatılmalı
Performans Optimizasyonu
Hasura ile büyük projelerde karşılaşabileceğin performans sorunları ve çözümleri:
- N+1 sorunu Hasura DataLoader kullandığı için büyük ölçüde önleniyor ama ilişkisel sorgularda yine de dikkatli olmak gerekiyor
- Query caching URQL veya Apollo’nun normalize cache’i ile client-side’da tekrar eden query’leri önleyebilirsin
- Subscription sayısı her client için bir WebSocket bağlantısı açılıyor, bunu minimize etmek gerekiyor
- Partial index kullanan filtreler için PostgreSQL index’lerini doğru tanımlamak kritik
- Read replica büyük okuma yükü varsa Hasura’yı read replica’ya yönlendirebilirsin
# PostgreSQL index ekleme migration örneği
hasura migrate create "add_performance_indexes" --database-name default
# up.sql
CREATE INDEX CONCURRENTLY idx_orders_user_id ON orders(user_id);
CREATE INDEX CONCURRENTLY idx_orders_status ON orders(status);
CREATE INDEX CONCURRENTLY idx_products_created_at ON products(created_at DESC);
CREATE INDEX CONCURRENTLY idx_order_items_order_id ON order_items(order_id);
CI/CD Pipeline Entegrasyonu
Hasura migration’larını CI/CD’ye dahil etmek production güvenilirliği için şart.
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
migrate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Hasura CLI Kur
run: curl -L https://github.com/hasura/graphql-engine/raw/stable/cli/get.sh | bash
- name: Migration'ları Uygula
run: |
cd hasura-project
hasura migrate apply --endpoint ${{ secrets.HASURA_ENDPOINT }}
--admin-secret ${{ secrets.HASURA_ADMIN_SECRET }}
--database-name default
hasura metadata apply --endpoint ${{ secrets.HASURA_ENDPOINT }}
--admin-secret ${{ secrets.HASURA_ADMIN_SECRET }}
- name: Next.js Deploy
run: |
cd frontend
npm ci
npm run build
Sonuç
Hasura ile Next.js kombinasyonu, özellikle hızlı MVP geliştirme ve orta ölçekli projelerde gerçekten işe yarıyor. Veritabanı schema’sını tanımlıyorsun, Hasura API’yi üretiyor, Next.js server component’leri ile type-safe şekilde bu API’yi consume ediyorsun. Kod üretimi sayesinde runtime’da tip hatalarıyla boğuşmak yerine geliştirme sürecinde yakalıyorsun.
Bununla birlikte her araç gibi Hasura’nın da sınırları var. Çok karmaşık business logic için event trigger ve action zinciri yönetmek karmaşıklaşabiliyor. Çok büyük projelerde vendor lock-in riski düşünülmeli. Subscription sayısı arttıkça WebSocket yönetimi kritik hale geliyor.
Söz konusu stack için şu yol haritasını öneririm: Local’de Docker Compose ile başla, Hasura CLI ile migration workflow’unu kur, Code Generator ile tip güvenliğini sağla, production’a geçmeden izin sistemini iyice test et ve index’lerini eksik bırakma. Bu adımları takip edersen sağlam bir temel üzerine inşa etmiş olursun.
