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_id kolonunu otomatik olarak X-Hasura-User-Id değ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.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir