İçeriğe geç
Sedat Demir
Geri dön

Hono + Cloudflare Workers + D1: Edge'de Tam Yığın Uygulama Geliştirme Rehberi

Hono + Cloudflare Workers + D1: Edge'de Tam Yığın Uygulama Geliştirme Rehberi

Kullanıcılarınıza en yakın noktadan, milisaniyeler içinde yanıt veren bir API hayal edin. Sunucu yönetimi yok, ölçeklendirme derdi yok, soğuk başlatma (cold start) neredeyse sıfır. Bu artık bir hayal değil — Hono + Cloudflare Workers + D1 üçlüsü ile edge computing'in tüm gücünü kullanabilirsiniz.

Bu yazıda, bu üç teknolojiyi bir araya getirerek sıfırdan tam yığın (full-stack) bir uygulama oluşturacağız. Gerçek kod örnekleri, veritabanı migrasyonları ve deployment süreçleriyle birlikte adım adım ilerleyeceğiz.


Edge Computing Nedir ve Neden Önemli?

Geleneksel sunucu mimarisinde uygulamanız belirli bir veri merkezinde çalışır. İstanbul'daki bir kullanıcı, ABD'deki bir sunucuya istek gönderdiğinde yüzlerce milisaniyelik gecikme yaşar. Edge computing, uygulamanızı kullanıcıya en yakın noktada çalıştırarak bu gecikmeyi dramatik şekilde azaltır.

Cloudflare'in global ağı 300'den fazla lokasyonda bulunur. Bu, kodunuzun dünyanın her yerinde eş zamanlı olarak çalıştığı anlamına gelir.


Teknoloji Yığınımızı Tanıyalım

Hono — Ultrafast Web Framework

Hono ("炎" Japonca'da "alev" anlamına gelir), edge runtime'lar için tasarlanmış ultrafast bir web framework'üdür. Express.js benzeri bir API sunarken, boyutu sadece ~14KB'dir.

Hono'nun öne çıkan özellikleri:

Cloudflare Workers — Serverless Edge Runtime

Cloudflare Workers, V8 isolate teknolojisi üzerine kurulmuş bir serverless platformdur. Lambda veya Cloud Functions'tan farklı olarak container başlatma süresi yoktur — cold start 0ms'dir.

D1 — Edge-Native SQLite Veritabanı

D1, Cloudflare'in edge'de çalışan SQLite tabanlı veritabanıdır. Geleneksel veritabanlarının aksine, verileriniz uygulamanızla aynı edge lokasyonlarında bulunur. SQL bilginiz varsa, hemen kullanmaya başlayabilirsiniz.


Proje Kurulumu

Hadi sıfırdan bir Görev Yönetim API'si (Task Manager) oluşturalım. CRUD operasyonlarının tamamını içerecek bu proje, gerçek dünya senaryolarını kapsamaktadır.

1. Projeyi Oluşturma

npm create hono@latest task-manager

CLI size runtime soracaktır — cloudflare-workers seçeneğini seçin.

cd task-manager
npm install

Proje yapınız şöyle görünecektir:

task-manager/
├── src/
│   └── index.ts
├── wrangler.toml
├── package.json
└── tsconfig.json

2. Wrangler Konfigürasyonu

wrangler.toml dosyasını D1 veritabanı bağlantısını içerecek şekilde güncelleyin:

name = "task-manager"
main = "src/index.ts"
compatibility_date = "2024-01-01"

[[d1_databases]]
binding = "DB"
database_name = "task-manager-db"
database_id = "" # Bu alanı birazdan dolduracağız

3. D1 Veritabanı Oluşturma

Terminal'den D1 veritabanınızı oluşturun:

npx wrangler d1 create task-manager-db

Bu komut size bir database_id döndürecektir. Bu ID'yi wrangler.toml dosyasındaki ilgili alana yapıştırın.

4. Veritabanı Şeması ve Migrasyon

migrations klasörü oluşturup ilk migrasyonumuzu ekleyelim:

mkdir migrations

migrations/0001_create_tables.sql dosyasını oluşturun:

-- migrations/0001_create_tables.sql

CREATE TABLE IF NOT EXISTS users (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  name TEXT NOT NULL,
  email TEXT UNIQUE NOT NULL,
  created_at TEXT DEFAULT (datetime('now'))
);

CREATE TABLE IF NOT EXISTS tasks (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  title TEXT NOT NULL,
  description TEXT,
  status TEXT DEFAULT 'pending' CHECK(status IN ('pending', 'in_progress', 'completed')),
  priority INTEGER DEFAULT 0 CHECK(priority BETWEEN 0 AND 3),
  user_id INTEGER NOT NULL,
  created_at TEXT DEFAULT (datetime('now')),
  updated_at TEXT DEFAULT (datetime('now')),
  FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);

CREATE INDEX idx_tasks_user_id ON tasks(user_id);
CREATE INDEX idx_tasks_status ON tasks(status);

Migrasyonu çalıştırın:

# Lokalde çalıştırmak için
npx wrangler d1 execute task-manager-db --local --file=./migrations/0001_create_tables.sql

# Production'da çalıştırmak için
npx wrangler d1 execute task-manager-db --file=./migrations/0001_create_tables.sql

Uygulama Kodunu Yazalım

Tip Tanımlamaları

Önce TypeScript tiplerini tanımlayalım. src/types.ts dosyasını oluşturun:

// src/types.ts

export interface Env {
  DB: D1Database;
}

export interface User {
  id: number;
  name: string;
  email: string;
  created_at: string;
}

export interface Task {
  id: number;
  title: string;
  description: string | null;
  status: 'pending' | 'in_progress' | 'completed';
  priority: number;
  user_id: number;
  created_at: string;
  updated_at: string;
}

export interface CreateTaskInput {
  title: string;
  description?: string;
  status?: Task['status'];
  priority?: number;
  user_id: number;
}

Ana Uygulama Dosyası

src/index.ts dosyasını aşağıdaki gibi düzenleyin:

// src/index.ts

import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { logger } from 'hono/logger';
import { prettyJSON } from 'hono/pretty-json';
import { HTTPException } from 'hono/http-exception';
import type { Env } from './types';
import { taskRoutes } from './routes/tasks';
import { userRoutes } from './routes/users';

const app = new Hono<{ Bindings: Env }>();

// Middleware'ler
app.use('*', logger());
app.use('*', cors());
app.use('*', prettyJSON());

// Sağlık kontrolü
app.get('/', (c) => {
  return c.json({
    message: 'Task Manager API - Edge üzerinde çalışıyor! 🔥',
    version: '1.0.0',
    endpoints: ['/api/users', '/api/tasks'],
  });
});

// Route'ları bağla
app.route('/api/users', userRoutes);
app.route('/api/tasks', taskRoutes);

// Global hata yakalama
app.onError((err, c) => {
  if (err instanceof HTTPException) {
    return c.json({ error: err.message }, err.status);
  }
  console.error('Beklenmeyen hata:', err);
  return c.json({ error: 'Sunucu hatası oluştu' }, 500);
});

// 404 handler
app.notFound((c) => {
  return c.json({ error: 'Endpoint bulunamadı' }, 404);
});

export default app;

Kullanıcı Route'ları

src/routes/users.ts dosyasını oluşturun:

// src/routes/users.ts

import { Hono } from 'hono';
import { HTTPException } from 'hono/http-exception';
import type { Env } from '../types';

export const userRoutes = new Hono<{ Bindings: Env }>();

// Tüm kullanıcıları getir
userRoutes.get('/', async (c) => {
  const { results } = await c.env.DB
    .prepare('SELECT * FROM users ORDER BY created_at DESC')
    .all();

  return c.json({ users: results });
});

// Kullanıcı oluştur
userRoutes.post('/', async (c) => {
  const { name, email } = await c.req.json();

  if (!name || !email) {
    throw new HTTPException(400, { message: 'name ve email zorunludur' });
  }

  try {
    const result = await c.env.DB
      .prepare('INSERT INTO users (name, email) VALUES (?, ?) RETURNING *')
      .bind(name, email)
      .first();

    return c.json({ user: result }, 201);
  } catch (error: any) {
    if (error.message?.includes('UNIQUE')) {
      throw new HTTPException(409, { message: 'Bu email zaten kayıtlı' });
    }
    throw error;
  }
});

// Tek kullanıcı + görevleri getir
userRoutes.get('/:id', async (c) => {
  const id = c.req.param('id');

  const user = await c.env.DB
    .prepare('SELECT * FROM users WHERE id = ?')
    .bind(id)
    .first();

  if (!user) {
    throw new HTTPException(404, { message: 'Kullanıcı bulunamadı' });
  }

  const { results: tasks } = await c.env.DB
    .prepare('SELECT * FROM tasks WHERE user_id = ? ORDER BY created_at DESC')
    .bind(id)
    .all();

  return c.json({ user, tasks });
});

Görev Route'ları

src/routes/tasks.ts dosyasını oluşturun:

// src/routes/tasks.ts

import { Hono } from 'hono';
import { HTTPException } from 'hono/http-exception';
import type { Env, CreateTaskInput } from '../types';

export const taskRoutes = new Hono<{ Bindings: Env }>();

// Tüm görevleri getir (filtreleme destekli)
taskRoutes.get('/', async (c) => {
  const status = c.req.query('status');
  const priority = c.req.query('priority');
  const page = parseInt(c.req.query('page') || '1');
  const limit = parseInt(c.req.query('limit') || '20');
  const offset = (page - 1) * limit;

  let query = 'SELECT tasks.*, users.name as user_name FROM tasks JOIN users ON tasks.user_id = users.id';
  const conditions: string[] = [];
  const params: any[] = [];

  if (status) {
    conditions.push('tasks.status = ?');
    params.push(status);
  }

  if (priority) {
    conditions.push('tasks.priority = ?');
    params.push(parseInt(priority));
  }

  if (conditions.length > 0) {
    query += ' WHERE ' + conditions.join(' AND ');
  }

  query += ' ORDER BY tasks.created_at DESC LIMIT ? OFFSET ?';
  params.push(limit, offset);

  const stmt = c.env.DB.prepare(query);
  const { results } = await stmt.bind(...params).all();

  // Toplam sayı
  let countQuery = 'SELECT COUNT(*) as total FROM tasks';
  if (conditions.length > 0) {
    countQuery += ' WHERE ' + conditions.join(' AND ');
  }
  const countStmt = c.env.DB.prepare(countQuery);
  const countResult = await countStmt.bind(...params.slice(0, -2)).first<{ total: number }>();

  return c.json({
    tasks: results,
    pagination: {
      page,
      limit,
      total: countResult?.total || 0,
      totalPages: Math.ceil((countResult?.total || 0) / limit),
    },
  });
});

// Görev oluştur
taskRoutes.post('/', async (c) => {
  const body: CreateTaskInput = await c.req.json();

  if (!body.title || !body.user_id) {
    throw new HTTPException(400, { message: 'title ve user_id zorunludur' });
  }

  // Kullanıcı var mı kontrol et
  const userExists = await c.env.DB
    .prepare('SELECT id FROM users WHERE id = ?')
    .bind(body.user_id)
    .first();

  if (!userExists) {
    throw new HTTPException(404, { message: 'Belirtilen kullanıcı bulunamadı' });
  }

  const result = await c.env.DB
    .prepare(`
      INSERT INTO tasks (title, description, status, priority, user_id)
      VALUES (?, ?, ?, ?, ?)
      RETURNING *
    `)
    .bind(
      body.title,
      body.description || null,
      body.status || 'pending',
      body.priority || 0,
      body.user_id
    )
    .first();

  return c.json({ task: result }, 201);
});

// Görev güncelle
taskRoutes.put('/:id', async (c) => {
  const id = c.req.param('id');
  const body = await c.req.json();

  const existing = await c.env.DB
    .prepare('SELECT * FROM tasks WHERE id = ?')
    .bind(id)
    .first();

  if (!existing) {
    throw new HTTPException(404, { message: 'Görev bulunamadı' });
  }

  const result = await c.env.DB
    .prepare(`
      UPDATE tasks
      SET title = ?, description = ?, status = ?, priority = ?, updated_at = datetime('now')
      WHERE id = ?
      RETURNING *
    `)
    .bind(
      body.title || existing.title,
      body.description ?? existing.description,
      body.status || existing.status,
      body.priority ?? existing.priority,
      id
    )
    .first();

  return c.json({ task: result });
});

// Görev sil
taskRoutes.delete('/:id', async (c) => {
  const id = c.req.param('id');

  const result = await c.env.DB
    .prepare('DELETE FROM tasks WHERE id = ? RETURNING id')
    .bind(id)
    .first();

  if (!result) {
    throw new HTTPException(404, { message: 'Görev bulunamadı' });
  }

  return c.json({ message: 'Görev silindi', id: result.id });
});

// Toplu durum güncelleme (Batch API örneği)
taskRoutes.patch('/batch/status', async (c) => {
  const { ids, status } = await c.req.json();

  if (!ids?.length || !status) {
    throw new HTTPException(400, { message: 'ids dizisi ve status zorunludur' });
  }

  const placeholders = ids.map(() => '?').join(',');
  const { changes } = await c.env.DB
    .prepare(`UPDATE tasks SET status = ?, updated_at = datetime('now') WHERE id IN (${placeholders})`)
    .bind(status, ...ids)
    .run();

  return c.json({
    message: `${changes} görev güncellendi`,
    updatedCount: changes,
  });
});

Middleware ile Güvenlik Katmanı

Gerçek dünya uygulamalarında API key doğrulaması gibi güvenlik katmanları şarttır. Hono'da bunu özel bir middleware ile yapabiliriz:

// src/middleware/auth.ts

import { Context, Next } from 'hono';
import { HTTPException } from 'hono/http-exception';

export const apiKeyAuth = async (c: Context, next: Next) => {
  const apiKey = c.req.header('X-API-Key');

  if (!apiKey || apiKey !== c.env.API_KEY) {
    throw new HTTPException(401, { message: 'Geçersiz API anahtarı' });
  }

  await next();
};

Bu middleware'i wrangler.toml dosyasındaki secret ile birlikte kullanın:

[vars]
API_KEY = "development-key-12345"

Production için:

npx wrangler secret put API_KEY

Lokal Geliştirme ve Deployment

Lokal Geliştirme

npx wrangler dev

Bu komut lokal bir geliştirme sunucusu başlatır. D1 veritabanı da lokal olarak simüle edilir.

API'yi Test Etme

# Kullanıcı oluştur
curl -X POST http://localhost:8787/api/users \
  -H "Content-Type: application/json" \
  -d '{"name": "Ahmet Yılmaz", "email": "ahmet@example.com"}'

# Görev oluştur
curl -X POST http://localhost:8787/api/tasks \
  -H "Content-Type: application/json" \
  -d '{"title": "Edge API geliştir", "description": "Hono ile REST API yaz", "priority": 2, "user_id": 1}'

# Görevleri filtrele
curl "http://localhost:8787/api/tasks?status=pending&page=1&limit=10"

Production'a Deploy

npx wrangler deploy

Bu kadar! Uygulamanız artık Cloudflare'in 300+ lokasyonunda çalışıyor.


Performans Karşılaştırması

Edge'de çalışmanın avantajını somutlaştıralım:

Metrik Geleneksel Sunucu Cloudflare Workers + D1
Cold Start 200-1000ms ~0ms
Ortalama Gecikme (Aynı bölge) 20-50ms 5-15ms
Ortalama Gecikme (Farklı kıta) 150-400ms 10-30ms
Ölçeklendirme Manuel/Otomatik Sınırsız, otomatik
Aylık maliyet (düşük trafik) $5-20 $0 (Free tier)

Production İçin En İyi Pratikler

  1. Prepared Statements kullanın: SQL injection'a karşı korunmanın yanı sıra D1'de performansı artırır.

  2. Hata yönetimini merkezileştirin: app.onError ile tüm hataları tek noktada yakalayın.

  3. Rate limiting ekleyin: Cloudflare'in yerleşik rate limiting özelliğini veya Hono middleware'ini kullanın.

  4. Batch operasyonlardan yararlanın: D1'in batch() metodu ile birden fazla sorguyu tek istekte çalıştırabilirsiniz:

const batchResults = await c.env.DB.batch([
  c.env.DB.prepare('SELECT COUNT(*) as total FROM tasks'),
  c.env.DB.prepare('SELECT COUNT(*) as pending FROM tasks WHERE status = ?').bind('pending'),
  c.env.DB.prepare('SELECT COUNT(*) as completed FROM tasks WHERE status = ?').bind('completed'),
]);
  1. Wrangler tail ile logları izleyin:
npx wrangler tail

Sonuç

Hono + Cloudflare Workers + D1 üçlüsü, modern web geliştirmenin en güçlü edge computing çözümlerinden birini oluşturuyor. Bu yazıda gördüğümüz gibi:

Geleneksel sunucu altyapılarının karmaşıklığından kurtulup, birkaç satır kod ve tek bir wrangler deploy komutuyla global ölçekte uygulama dağıtabilirsiniz. Üstelik Cloudflare'in ücretsiz katmanı, küçük ve orta ölçekli projeler için hiçbir maliyet gerektirmez.

Edge computing artık gelecek değil — şimdi. Bir sonraki projenizde bu yığını denemenizi şiddetle tavsiye ederim.


Share this post on:

Sonraki Yazı
Drizzle ORM Derinlemesine: TypeScript'in En İyi ORM'i mi?