Hasura ile Dakikalar İçinde GraphQL API Oluşturma
Veritabanın var, projeni yazmak için saatler harcayacaksın, REST endpoint’leri tek tek yazacaksın, her biri için ayrı ayrı authentication, pagination, filtering ekleyeceksin… Tanıdık bir senaryo değil mi? İşte tam bu noktada Hasura devreye giriyor ve sana şunu söylüyor: “Bırak bunları, ben hallederim.” Bu yazıda Hasura’yı sıfırdan kurarak dakikalar içinde production’a yakın bir GraphQL API’nin nasıl oluşturulacağını adım adım anlatacağım.
Hasura Nedir ve Neden Kullanmalısın?
Hasura, PostgreSQL (ve artık birçok başka veritabanı) üzerine otomatik olarak GraphQL API üreten açık kaynaklı bir engine’dir. Tablolarını gösterir, ilişkilerini anlar ve sana eksiksiz bir CRUD API sunar. Subscription desteği sayesinde realtime özellikler de cabası.
Klasik yaklaşımda bir Node.js ya da Python projesi açar, ORM tanımlar, resolver yazarsın, middleware eklersin… Hasura’da ise PostgreSQL’de tablo oluşturursun, Hasura bağlarsın, bitti. Gerisi otomatik gelir.
Peki bu sihir nerede işe yaramaz? Çok karmaşık iş mantığı olan projelerde Hasura’yı tek başına kullanmak yetersiz kalabilir. Ama bu durumda bile Hasura’nın Action ve Remote Schema özelliklerini kullanarak custom logic ekleyebilirsin. Yani Hasura “ya hep ya hiç” değil, ihtiyacına göre şekillenen bir araç.
Kurulum: Docker ile Hızlı Başlangıç
Geliştirme ortamı için Docker Compose ile kurmak en pratik yol. Production için de benzer bir yapı kullanabilirsin ama orada ek güvenlik katmanları şart.
mkdir hasura-project && cd hasura-project
cat > docker-compose.yml << 'EOF'
version: '3.8'
services:
postgres:
image: postgres:15
restart: always
volumes:
- db_data:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: mysecretpassword
POSTGRES_DB: myapp
ports:
- "5432:5432"
graphql-engine:
image: hasura/graphql-engine:v2.36.0
ports:
- "8080:8080"
depends_on:
- postgres
restart: always
environment:
HASURA_GRAPHQL_DATABASE_URL: postgres://postgres:mysecretpassword@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: myadminsecret
volumes:
db_data:
EOF
docker compose up -d
Birkaç saniye bekledikten sonra http://localhost:8080/console adresine gittiğinde Hasura Console seni karşılıyor. Admin secret olarak myadminsecret kullanacaksın.
Bu kadar. GraphQL engine ayakta.
Gerçek Dünya Senaryosu: E-ticaret Veritabanı
Teorik örnekler sıkıcı olduğu için gerçek bir senaryo üzerinden gidelim. Küçük bir e-ticaret platformu kuracağız: kullanıcılar, ürünler, siparişler ve sipariş kalemleri.
Hasura Console’da “Data” sekmesine git, “SQL” bölümünü aç ve şu şemayı çalıştır:
-- Kullanicilar tablosu
CREATE TABLE users (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
name VARCHAR(255) NOT NULL,
role VARCHAR(50) DEFAULT 'customer',
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Kategoriler tablosu
CREATE TABLE categories (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
slug VARCHAR(255) UNIQUE NOT NULL,
parent_id INTEGER REFERENCES categories(id)
);
-- Urunler tablosu
CREATE TABLE products (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
name VARCHAR(500) NOT NULL,
description TEXT,
price NUMERIC(10,2) NOT NULL,
stock_quantity INTEGER DEFAULT 0,
category_id INTEGER REFERENCES categories(id),
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Siparisler tablosu
CREATE TABLE orders (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
user_id UUID REFERENCES users(id) NOT NULL,
status VARCHAR(50) DEFAULT 'pending',
total_amount NUMERIC(10,2) NOT NULL,
shipping_address JSONB,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Siparis kalemleri tablosu
CREATE TABLE order_items (
id SERIAL PRIMARY KEY,
order_id UUID REFERENCES orders(id) NOT NULL,
product_id UUID REFERENCES products(id) NOT NULL,
quantity INTEGER NOT NULL,
unit_price NUMERIC(10,2) NOT NULL
);
-- Otomatik updated_at guncelleme fonksiyonu
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ language 'plpgsql';
CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_orders_updated_at BEFORE UPDATE ON orders
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
“Run!” butonuna bastıktan sonra Hasura tablolarını otomatik olarak tanıyor. Şimdi “Track All” butonuna tıkla; Hasura tüm tabloları ve foreign key ilişkilerini otomatik olarak GraphQL şemasına ekliyor.
İlişkileri Tanımlamak
Tablolar track edildikten sonra “Data > [tablo adı] > Relationships” sekmesinde Hasura sana önerilen ilişkileri gösteriyor. Bunları tek tıkla ekleyebilirsin. Ama ne olduğunu anlaman için manuel de ekleyebilirsin.
Object Relationship (Many-to-One): Bir sipariş bir kullanıcıya ait. Array Relationship (One-to-Many): Bir kullanıcının birden fazla siparişi var.
Bunları ekledikten sonra şu gibi nested query’ler çalışmaya başlıyor:
query GetOrderWithDetails {
orders(
where: { status: { _eq: "pending" } }
order_by: { created_at: desc }
limit: 10
) {
id
status
total_amount
created_at
user {
name
email
}
order_items {
quantity
unit_price
product {
name
category {
name
}
}
}
}
}
Bu query’yi yazmak için tek satır backend kodu yazmadın. Hasura ilişkileri takip etti, JOIN’leri oluşturdu, sonucu nested JSON olarak sana verdi.
Hasura CLI ile Migration Yönetimi
Console üzerinden değişiklik yapmak hızlı ama production ortamında migration yönetimi şart. Hasura CLI bu iş için biçilmiş kaftan.
# Hasura CLI kurulumu
curl -L https://github.com/hasura/graphql-engine/raw/stable/cli/get.sh | bash
# Proje baslangici
hasura init myapp --endpoint http://localhost:8080 --admin-secret myadminsecret
cd myapp
# Mevcut durumu export et
hasura metadata export
# Migration olustur
hasura migrate create "add_product_reviews" --database-name default
# Migration dosyasini doldur
cat > migrations/default/[timestamp]_add_product_reviews/up.sql << 'EOF'
CREATE TABLE product_reviews (
id SERIAL PRIMARY KEY,
product_id UUID REFERENCES products(id) NOT NULL,
user_id UUID REFERENCES users(id) NOT NULL,
rating INTEGER CHECK (rating >= 1 AND rating <= 5),
comment TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
EOF
# Migration uygula
hasura migrate apply
# Metadata'yi uygula
hasura metadata apply
Bu yaklaşım sayesinde değişikliklerin git’e giriyor, takım arkadaşların aynı migration’ları kendi ortamlarında uygulayabiliyor ve production deployment’ı kontrollü hale geliyor.
Authorization: Row Level Security ile Güvenlik
Hasura’nın en güçlü özelliklerinden biri permission sistemi. Her tablo için rol bazlı izinler tanımlayabilirsin. Örneğin bir kullanıcı sadece kendi siparişlerini görebilmeli.
Console’da “Data > orders > Permissions” sekmesine git ve customer rolü için şu ayarları yap:
Select Permission:
- Row-level permission:
{"user_id": {"_eq": "X-Hasura-User-Id"}} - Column permission: id, status, total_amount, shipping_address, created_at kolonlarına erişim ver
Insert Permission:
- Row-level check:
{"user_id": {"_eq": "X-Hasura-User-Id"}} - Column preset:
user_idkolonunu otomatik olarakX-Hasura-User-Iddeğeriyle doldur
Bunu CLI ile de yapabilirsin:
# metadata/databases/default/tables/public_orders.yaml
table:
name: orders
schema: public
select_permissions:
- permission:
columns:
- id
- status
- total_amount
- shipping_address
- created_at
filter:
user_id:
_eq: X-Hasura-User-Id
role: customer
insert_permissions:
- permission:
check:
user_id:
_eq: X-Hasura-User-Id
columns:
- status
- total_amount
- shipping_address
set:
user_id: x-hasura-user-id
role: customer
JWT token’ında X-Hasura-User-Id ve X-Hasura-Role claim’leri olması yeterli. Hasura gerisini hallediyor.
JWT Authentication Entegrasyonu
Hasura’ya JWT doğrulamasını aktif etmek için:
# docker-compose.yml environment bolumune ekle
HASURA_GRAPHQL_JWT_SECRET: '{"type":"HS256","key":"your-super-secret-key-minimum-32-chars"}'
Test için örnek bir JWT oluşturalım:
# jwt.io veya bu python scripti ile test token olustur
python3 << 'EOF'
import jwt
import datetime
payload = {
"sub": "1234567890",
"name": "Ahmet Yilmaz",
"iat": datetime.datetime.utcnow(),
"exp": datetime.datetime.utcnow() + datetime.timedelta(hours=24),
"https://hasura.io/jwt/claims": {
"x-hasura-allowed-roles": ["customer", "user"],
"x-hasura-default-role": "customer",
"x-hasura-user-id": "550e8400-e29b-41d4-a716-446655440000"
}
}
token = jwt.encode(payload, "your-super-secret-key-minimum-32-chars", algorithm="HS256")
print(token)
EOF
Bu token’ı Authorization: Bearer header’ı olarak gönderdiğinde Hasura kullanıcıyı tanıyacak ve permission kurallarını ona göre uygulayacak.
Actions ile Custom Business Logic
Hasura her şeyi halledemez. Ödeme işlemi, email gönderimi, karmaşık hesaplamalar bunlar için Actions kullanıyorsun. Action esasen GraphQL mutation ya da query’yi bir HTTP endpoint’e yönlendiriyor.
Örnek senaryo: Sipariş tamamlandığında stok düşürme ve ödeme işlemi için:
# Action tanımı (Console > Actions > Create)
type Mutation {
placeOrder(
items: [OrderItemInput!]!
shipping_address: jsonb!
): PlaceOrderOutput
}
input OrderItemInput {
product_id: uuid!
quantity: Int!
}
type PlaceOrderOutput {
order_id: uuid!
total_amount: numeric!
status: String!
}
Bu action’ı karşılayan bir handler yazalım:
// actions/place-order.js (Node.js Express)
const express = require('express');
const app = express();
app.use(express.json());
app.post('/place-order', async (req, res) => {
const { items, shipping_address } = req.body.input;
const userId = req.body.session_variables['x-hasura-user-id'];
try {
// Urunleri ve stoklari kontrol et
const productIds = items.map(i => i.product_id);
const checkStockQuery = `
query CheckStock($ids: [uuid!]!) {
products(where: { id: { _in: $ids }, is_active: { _eq: true } }) {
id
price
stock_quantity
name
}
}
`;
const stockResponse = await fetch('http://graphql-engine:8080/v1/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-hasura-admin-secret': process.env.HASURA_ADMIN_SECRET
},
body: JSON.stringify({ query: checkStockQuery, variables: { ids: productIds } })
});
const { data } = await stockResponse.json();
const products = data.products;
// Stok kontrolu
for (const item of items) {
const product = products.find(p => p.id === item.product_id);
if (!product || product.stock_quantity < item.quantity) {
return res.status(400).json({
message: `${product?.name || 'Urun'} icin yeterli stok yok`
});
}
}
// Toplam tutari hesapla
const totalAmount = items.reduce((sum, item) => {
const product = products.find(p => p.id === item.product_id);
return sum + (product.price * item.quantity);
}, 0);
// Siparisi olustur (admin secret ile, permission bypass)
const createOrderMutation = `
mutation CreateOrder($userId: uuid!, $total: numeric!, $address: jsonb!, $items: [order_items_insert_input!]!) {
insert_orders_one(object: {
user_id: $userId
total_amount: $total
shipping_address: $address
status: "confirmed"
order_items: { data: $items }
}) {
id
total_amount
status
}
}
`;
const orderItems = items.map(item => ({
product_id: item.product_id,
quantity: item.quantity,
unit_price: products.find(p => p.id === item.product_id).price
}));
const orderResponse = await fetch('http://graphql-engine:8080/v1/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-hasura-admin-secret': process.env.HASURA_ADMIN_SECRET
},
body: JSON.stringify({
query: createOrderMutation,
variables: {
userId,
total: totalAmount,
address: shipping_address,
items: orderItems
}
})
});
const orderData = await orderResponse.json();
const order = orderData.data.insert_orders_one;
res.json({
order_id: order.id,
total_amount: order.total_amount,
status: order.status
});
} catch (error) {
console.error('Order creation error:', error);
res.status(500).json({ message: 'Siparis olusturulurken hata olustu' });
}
});
app.listen(3000, () => console.log('Action handler port 3000 uzerinde calisiyor'));
Event Triggers: Asenkron İşlemler
Siparişin oluşturulduğunda email göndermek istiyorsun ama bunu senkron yapmak istemiyorsun. Event Triggers tam bu iş için:
# Hasura Console > Events > Create Trigger
# Trigger adi: order_created
# Tablo: orders
# Event: INSERT
# Webhook URL: http://notification-service:4000/on-order-created
Payload otomatik olarak şu formatta geliyor:
{
"event": {
"op": "INSERT",
"data": {
"old": null,
"new": {
"id": "...",
"user_id": "...",
"status": "confirmed",
"total_amount": "299.99"
}
}
},
"table": { "schema": "public", "name": "orders" },
"trigger": { "name": "order_created" }
}
Webhook handler’ında bu veriyi alıp email, SMS ya da push notification gönderebilirsin. Hasura retry mekanizmasıyla başarısız webhook’ları otomatik olarak tekrar deniyor.
Performans: Caching ve Query Optimization
Production’da dikkat edilmesi gereken birkaç nokta var.
Query Depth Limiting: Sonsuz nested query saldırılarını önlemek için:
HASURA_GRAPHQL_DEPTH_LIMIT: "10"
HASURA_GRAPHQL_NODE_LIMIT: "1000"
Response Caching: Hasura Enterprise’da built-in cache var ama community version için şu header’ı kullanabilirsin:
query GetCategories @cached(ttl: 300) {
categories {
id
name
slug
}
}
PostgreSQL Index’leri: Hasura ne kadar akıllı olursa olsun yavaş bir veritabanını kurtaramaz. Sık kullanılan kolonlara index ekle:
-- Sik sorgulanan kolonlara index
CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_orders_status ON orders(status);
CREATE INDEX idx_orders_created_at ON orders(created_at DESC);
CREATE INDEX idx_products_category_id ON products(category_id);
CREATE INDEX idx_products_is_active ON products(is_active) WHERE is_active = true;
-- Composite index ornegi
CREATE INDEX idx_order_items_order_product ON order_items(order_id, product_id);
Analyze komutuyla query planını kontrol et:
# Hasura Console SQL sekmesinde
EXPLAIN ANALYZE
SELECT o.*, u.name, u.email
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.status = 'pending'
ORDER BY o.created_at DESC
LIMIT 20;
Monitoring ve Health Check
# Hasura health endpoint
curl http://localhost:8080/healthz
# Metadata versiyonunu kontrol et
curl -H "X-Hasura-Admin-Secret: myadminsecret"
http://localhost:8080/v1/metadata
-d '{"type":"export_metadata","args":{}}'
# Metrics endpoint (Prometheus formatinda)
curl http://localhost:8080/v1/metrics
-H "X-Hasura-Admin-Secret: myadminsecret"
Docker Compose’a bir basit Prometheus + Grafana ekleyip Hasura metriklerini izlemek, özellikle request latency ve error rate’i takip etmek production’da sağolsun dedirtir.
Hasura Cloud vs Self-Hosted
Hızlıca karşılaştıralım:
Self-Hosted avantajları:
- Tam kontrol, veri hiçbir zaman dışarı çıkmaz
- Lisans maliyeti yok (community features)
- Kendi scaling stratejini uygularsın
Self-Hosted dezavantajları:
- Yüksek erişilebilirlik için ekstra iş
- Upgrade, monitoring, backup sana düşer
Hasura Cloud avantajları:
- Managed infrastructure, sıfır ops yükü
- Built-in caching, rate limiting, observability
- Otomatik scaling
Küçük-orta ölçekli projelerde Hasura Cloud ücretsiz tier’ı deneyip sonra self-hosted’a geçmek mantıklı bir yol. Büyük kurumsal projelerde ise self-hosted ya da Hasura Enterprise tercih ediliyor.
Sonuç
Hasura, “API yazmak” yerine “API tasarlamak” paradigmasına geçmeni sağlıyor. PostgreSQL şemanı düzgün tasarladığın an, filtreleme, sıralama, pagination, ilişkili sorgular, subscription ve temel CRUD işlemleri otomatik olarak geliyor. Geriye yalnızca gerçek iş mantığın kalıyor; onu da Actions ve Event Triggers ile kolayca entegre ediyorsun.
Benim favori kullanım senaryom şu: Startup ya da MVP aşamasındaki projelerde Hasura ile başla, hızlıca bir API elde et, ürünü validate et. Ürün büyüdükçe ve iş mantığı karmaşıklaştıkça bazı parçaları custom servislerle değiştir ya da Remote Schema ile federasyon yap. Bu yaklaşım hem geliştirme hızını koruyor hem de uzun vadede teknik borç biriktirmiyor.
Tek uyarım şu: Hasura’yı production’a atmadan önce permission katmanını çok dikkatli test et. Row-level security’yi doğru yapılandırmadan yayına alınan sistemlerde veri sızıntısı riski var. Her rol için izinleri gözden geçir, JWT claim’lerini doğrula ve admin secret’ı asla client tarafında kullanma.
