Deno Deploy ile Edge Function Yazma ve Test Etme
Sunucusuz mimari denince aklıma hep şu soru geliyor: “Peki bu fonksiyon dünyanın hangi köşesinde çalışıyor?” Klasik serverless yaklaşımında bu sorunun pek önemi yoktu, ama edge computing’e geçince her şey değişti. Deno Deploy tam da bu noktada devreye giriyor; yazdığın kodu 35’ten fazla lokasyonda, kullanıcıya en yakın noktada çalıştırıyor. Bu yazıda Deno Deploy ile edge function yazmanın pratik taraflarını, test süreçlerini ve gerçek dünya senaryolarını ele alacağız.
Deno Deploy Nedir ve Neden Tercih Edilmeli?
Deno Deploy, Deno ekibinin geliştirdiği bir edge computing platformu. Temelde şunu yapıyor: Deno runtime’ını dünyanın farklı noktalarındaki veri merkezlerinde çalıştırıyor ve senin yazdığın TypeScript/JavaScript kodunu bu noktalarda deploy ediyor. Geleneksel bir sunucu kurman gerekmiyor, Docker image hazırlamana gerek yok, Kubernetes cluster’ı yönetmene hiç gerek yok.
Peki neden Vercel Edge veya Cloudflare Workers yerine Deno Deploy? Birkaç somut neden:
- Web standartlarına tam uyum: Fetch API, Web Crypto, URL, ReadableStream gibi standart API’ları native olarak kullanabiliyorsun
- TypeScript first: Transpile adımı olmadan direkt TypeScript çalıştırıyor
- Deno’nun güvenlik modeli: İzin tabanlı sistem edge’de de geçerli
- GitHub entegrasyonu: Push’la birlikte otomatik deploy
- Ücretsiz tier: Geliştirme ve küçük projeler için oldukça cömert
Ortamı Hazırlama
Başlamadan önce local Deno kurulumuna ihtiyacın var. Deno Deploy’a push etmeden önce her şeyi locally test edeceğiz.
# Linux/macOS için Deno kurulumu
curl -fsSL https://deno.land/install.sh | sh
# Kurulumu doğrula
deno --version
# Deno Deploy CLI'ı kur (deployctl)
deno install --allow-read --allow-write --allow-env --allow-net --allow-run
--no-check -r -f https://deno.land/x/deploy/deployctl.ts
# deployctl'i doğrula
deployctl --version
Deno Deploy hesabı açmak için [deno.com/deploy](https://deno.com/deploy) adresine git ve GitHub hesabınla giriş yap. Sonrasında bir proje oluştur ve personal access token’ını al. Bu token’ı environment variable olarak ayarlayacaksın:
# Token'ı export et (veya .bashrc/.zshrc'ye ekle)
export DENO_DEPLOY_TOKEN="your_token_here"
# Proje ismine de ihtiyacın olacak
export DENO_PROJECT="your-project-name"
İlk Edge Function’ı Yazmak
Basit bir “hello world” ile başlayalım ama biraz daha anlamlı hale getirelim. Kullanıcının IP adresini ve yaklaşık lokasyonunu döndüren bir endpoint yazalım.
// main.ts
Deno.serve(async (req: Request): Promise<Response> => {
const url = new URL(req.url);
// Deno Deploy otomatik olarak bu header'ları ekliyor
const country = req.headers.get("x-forwarded-for") || "unknown";
const region = req.headers.get("cf-ipcountry") ||
req.headers.get("x-vercel-ip-country") || "unknown";
if (url.pathname === "/health") {
return new Response(JSON.stringify({ status: "ok", ts: Date.now() }), {
headers: { "content-type": "application/json" },
});
}
if (url.pathname === "/whoami") {
const data = {
ip: country,
country: region,
userAgent: req.headers.get("user-agent") || "unknown",
// Deno Deploy hangi region'da çalıştığını söyler
deployRegion: Deno.env.get("DENO_REGION") || "local",
timestamp: new Date().toISOString(),
};
return new Response(JSON.stringify(data, null, 2), {
headers: {
"content-type": "application/json",
"cache-control": "no-store",
},
});
}
return new Response("Not Found", { status: 404 });
});
Bu kodu local’de test etmek için:
# Local'de çalıştır
deno run --allow-net main.ts
# Başka terminal'den test et
curl http://localhost:8000/health
curl http://localhost:8000/whoami
Middleware Pattern ile Request Pipeline
Edge’de çalışırken middleware pattern çok işe yarıyor. Authentication, rate limiting, logging gibi cross-cutting concern’leri ayrı katmanlara taşımak kodu sürdürülebilir yapıyor.
// middleware.ts
type Handler = (req: Request) => Promise<Response>;
type Middleware = (next: Handler) => Handler;
// CORS middleware
export const corsMiddleware = (allowedOrigins: string[]): Middleware => {
return (next: Handler): Handler => {
return async (req: Request): Promise<Response> => {
const origin = req.headers.get("origin") || "";
const isAllowed = allowedOrigins.includes("*") ||
allowedOrigins.includes(origin);
// OPTIONS preflight request'i handle et
if (req.method === "OPTIONS") {
return new Response(null, {
status: 204,
headers: {
"access-control-allow-origin": isAllowed ? origin : "",
"access-control-allow-methods": "GET, POST, PUT, DELETE, OPTIONS",
"access-control-allow-headers": "Content-Type, Authorization",
"access-control-max-age": "86400",
},
});
}
const response = await next(req);
if (isAllowed) {
const newHeaders = new Headers(response.headers);
newHeaders.set("access-control-allow-origin", origin);
return new Response(response.body, {
status: response.status,
headers: newHeaders,
});
}
return response;
};
};
};
// Auth middleware - Bearer token kontrolü
export const authMiddleware = (secretKey: string): Middleware => {
return (next: Handler): Handler => {
return async (req: Request): Promise<Response> => {
const authHeader = req.headers.get("authorization");
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return new Response(
JSON.stringify({ error: "Unauthorized" }),
{
status: 401,
headers: { "content-type": "application/json" }
}
);
}
const token = authHeader.slice(7);
// Basit token doğrulama - production'da JWT kullan
if (token !== secretKey) {
return new Response(
JSON.stringify({ error: "Invalid token" }),
{
status: 403,
headers: { "content-type": "application/json" }
}
);
}
return next(req);
};
};
};
// Middleware'leri compose et
export const compose = (...middlewares: Middleware[]) => {
return (handler: Handler): Handler => {
return middlewares.reduceRight(
(acc, middleware) => middleware(acc),
handler
);
};
};
Gerçek Dünya Senaryosu: API Gateway
Şimdi daha gerçekçi bir senaryo: Birden fazla downstream servise istek yönlendiren bir API gateway yazalım.
// api-gateway.ts
import { corsMiddleware, authMiddleware, compose } from "./middleware.ts";
const SERVICES: Record<string, string> = {
users: Deno.env.get("USERS_SERVICE_URL") || "https://users-api.internal",
products: Deno.env.get("PRODUCTS_SERVICE_URL") || "https://products-api.internal",
orders: Deno.env.get("ORDERS_SERVICE_URL") || "https://orders-api.internal",
};
async function proxyRequest(
req: Request,
targetUrl: string
): Promise<Response> {
const url = new URL(req.url);
const target = new URL(targetUrl);
target.pathname = url.pathname;
target.search = url.search;
// Downstream'e iletilmeyecek header'ları temizle
const headers = new Headers(req.headers);
headers.delete("host");
headers.set("x-forwarded-for",
req.headers.get("x-forwarded-for") || "unknown");
headers.set("x-gateway-region",
Deno.env.get("DENO_REGION") || "local");
try {
const response = await fetch(target.toString(), {
method: req.method,
headers,
body: req.method !== "GET" && req.method !== "HEAD"
? req.body
: null,
});
return new Response(response.body, {
status: response.status,
headers: response.headers,
});
} catch (err) {
console.error(`Proxy error to ${targetUrl}:`, err);
return new Response(
JSON.stringify({ error: "Service unavailable" }),
{
status: 503,
headers: { "content-type": "application/json" }
}
);
}
}
const handler = async (req: Request): Promise<Response> => {
const url = new URL(req.url);
const segments = url.pathname.split("/").filter(Boolean);
if (segments.length === 0) {
return new Response(
JSON.stringify({
message: "API Gateway",
services: Object.keys(SERVICES)
}),
{ headers: { "content-type": "application/json" } }
);
}
const service = segments[0];
const serviceUrl = SERVICES[service];
if (!serviceUrl) {
return new Response(
JSON.stringify({ error: `Unknown service: ${service}` }),
{
status: 404,
headers: { "content-type": "application/json" }
}
);
}
return proxyRequest(req, serviceUrl);
};
// Public endpoint'ler için auth bypass
const SECRET_KEY = Deno.env.get("API_SECRET_KEY") || "dev-secret";
const gatewayHandler = (req: Request): Promise<Response> => {
const url = new URL(req.url);
// /users/public gibi public endpoint'ler auth istemez
if (url.pathname.includes("/public/")) {
return compose(corsMiddleware(["*"]))(handler)(req);
}
return compose(
corsMiddleware(["https://app.example.com"]),
authMiddleware(SECRET_KEY)
)(handler)(req);
};
Deno.serve(gatewayHandler);
Test Stratejisi
Edge function test etmek biraz farklı düşünmek gerektiriyor. Deno’nun yerleşik test runner’ı var ve bunu maksimum kullanmalıyız.
// main_test.ts
import { assertEquals, assertMatch } from "https://deno.land/std/assert/mod.ts";
// Test yardımcı fonksiyonu - mock request oluşturur
function createRequest(
path: string,
options: RequestInit & { headers?: Record<string, string> } = {}
): Request {
return new Request(`http://localhost:8000${path}`, {
...options,
headers: new Headers({
"content-type": "application/json",
...options.headers,
}),
});
}
Deno.test("GET /health - başarılı yanıt döner", async () => {
// Handler'ı direkt import edip test ediyoruz
const { default: handler } = await import("./handler.ts");
const req = createRequest("/health");
const res = await handler(req);
assertEquals(res.status, 200);
const body = await res.json();
assertEquals(body.status, "ok");
});
Deno.test("GET /whoami - JSON response döner", async () => {
const { default: handler } = await import("./handler.ts");
const req = createRequest("/whoami", {
headers: {
"user-agent": "TestBot/1.0",
"x-forwarded-for": "1.2.3.4",
},
});
const res = await handler(req);
assertEquals(res.status, 200);
assertEquals(
res.headers.get("content-type"),
"application/json"
);
const body = await res.json();
assertMatch(body.timestamp, /^d{4}-d{2}-d{2}T/);
});
Deno.test("Tanımlanmamış endpoint 404 döner", async () => {
const { default: handler } = await import("./handler.ts");
const req = createRequest("/bu-endpoint-yok");
const res = await handler(req);
assertEquals(res.status, 404);
});
Testleri çalıştırmak için:
# Tüm testleri çalıştır
deno test --allow-net --allow-env
# Belirli bir dosyayı test et
deno test --allow-net main_test.ts
# Watch mode'da test et (TDD için ideal)
deno test --allow-net --watch
# Coverage raporu al
deno test --allow-net --coverage=cov_profile
deno coverage cov_profile --lcov > coverage.lcov
Deploy ve CI/CD Entegrasyonu
GitHub Actions ile otomatik deploy pipeline kuralım. Her push’ta test çalışsın, main branch’e merge olunca deploy gerçekleşsin.
# .github/workflows/deploy.yml içeriği
# (YAML formatında gösteriyoruz ama bash bloğu olarak)
# GitHub Actions workflow dosyası:
# name: Test and Deploy
# on:
# push:
# branches: [main, develop]
# pull_request:
# branches: [main]
# Manuel deploy için:
deployctl deploy
--project=$DENO_PROJECT
--token=$DENO_DEPLOY_TOKEN
--prod
main.ts
# Preview deploy (PR'lar için)
deployctl deploy
--project=$DENO_PROJECT
--token=$DENO_DEPLOY_TOKEN
main.ts
# Deploy edilen URL'i al ve smoke test yap
DEPLOY_URL=$(deployctl deploy --project=$DENO_PROJECT --token=$DENO_DEPLOY_TOKEN main.ts 2>&1 | grep "https://" | tail -1)
echo "Deploy URL: $DEPLOY_URL"
# Smoke test
curl -f "$DEPLOY_URL/health" || exit 1
echo "Smoke test passed!"
Deno Deploy’un dashboard’undan da şunları takip edebilirsin:
- Her deployment’ın log’larını realtime görme
- Request metrics ve latency grafikleri
- Region bazlı traffic dağılımı
- Environment variable yönetimi
KV Store ile State Yönetimi
Edge function’lar stateless ama bazen state tutmak gerekiyor. Deno Deploy’un built-in KV store’u tam burada devreye giriyor.
// rate-limiter.ts - KV ile rate limiting
const kv = await Deno.openKv();
interface RateLimitConfig {
maxRequests: number;
windowSeconds: number;
}
async function checkRateLimit(
identifier: string,
config: RateLimitConfig
): Promise<{ allowed: boolean; remaining: number; resetAt: number }> {
const key = ["rate_limit", identifier];
const windowMs = config.windowSeconds * 1000;
const now = Date.now();
const windowStart = now - windowMs;
// Atomic transaction ile race condition'ı önle
const entry = await kv.get<{ count: number; windowStart: number }>(key);
let count = 0;
let currentWindowStart = now;
if (entry.value && entry.value.windowStart > windowStart) {
count = entry.value.count;
currentWindowStart = entry.value.windowStart;
}
const resetAt = currentWindowStart + windowMs;
if (count >= config.maxRequests) {
return {
allowed: false,
remaining: 0,
resetAt
};
}
// Sayacı güncelle
await kv.set(key, {
count: count + 1,
windowStart: currentWindowStart,
}, { expireIn: windowMs });
return {
allowed: true,
remaining: config.maxRequests - count - 1,
resetAt,
};
}
// Rate limiter'ı kullanan handler
Deno.serve(async (req: Request): Promise<Response> => {
const ip = req.headers.get("x-forwarded-for") || "anonymous";
const rateLimit = await checkRateLimit(ip, {
maxRequests: 100,
windowSeconds: 60,
});
if (!rateLimit.allowed) {
return new Response(
JSON.stringify({ error: "Too many requests" }),
{
status: 429,
headers: {
"content-type": "application/json",
"retry-after": Math.ceil((rateLimit.resetAt - Date.now()) / 1000).toString(),
"x-ratelimit-remaining": "0",
"x-ratelimit-reset": rateLimit.resetAt.toString(),
},
}
);
}
// Normal işlemi devam ettir
return new Response(
JSON.stringify({ message: "OK", remaining: rateLimit.remaining }),
{ headers: { "content-type": "application/json" } }
);
});
Performans İpuçları ve Production Dikkat Noktaları
Edge function’larla çalışırken birkaç şeyi aklında bulundurman gerekiyor:
- Cold start’ı minimize et: Global scope’da pahalı işlemler yapma.
Deno.servecallback’i dışında kalan kod sadece ilk başlatmada çalışır, bunu avantajına kullan - Streaming response kullan: Büyük yanıtlar için
ReadableStreamile streaming yap, kullanıcı ilk byte’ı hemen görsün - KV okumalarını paralel yap:
Promise.allile birden fazla KV operasyonunu aynı anda başlat - Cache header’larını doğru ayarla: Edge’de cache çok önemli,
cache-controlheader’larını ihmal etme - Error boundary koy: Her async işlemi try/catch ile sar, bir hata tüm fonksiyonu patlatmasın
- Log stratejisi:
console.logkullan ama production’da hassas veri loglama. Deno Deploy log’ları dashboard’dan görünür - Environment variable doğrulaması: Uygulama başlarken tüm gerekli env var’ları kontrol et, eksikse hemen çık
- Memory’e dikkat: Edge function’lar sınırlı memory’de çalışır, büyük veri işlemek için streaming tercih et
Deployment öncesi bir checklist olarak şunları yapman önerilen:
deno check main.tsile type check yapdeno lintile kod kalitesini kontrol etdeno fmtile formatlama yap- Testlerin %80’in üstünde coverage’a ulaştığından emin ol
- Smoke test senaryolarını hazırla
- Rollback planını önceden düşün
Sonuç
Deno Deploy ile edge function geliştirmek, modern web altyapısının en temiz yaklaşımlarından biri. TypeScript’i native çalıştırması, web standartlarına tam uyumu ve sıfır konfigürasyonla başlayabilme özelliği onu gerçekten çekici yapıyor. Üstelik built-in KV store ile stateless sınırlamasını da aşabiliyorsun.
Öğrenme eğrisi düşük, ama production’da başarılı olmak için middleware pattern’larını iyi kavramak, test stratejini doğru kurmak ve edge’e özgü kısıtlamaları (memory, cold start, KV latency) göz önünde bulundurmak şart. Bu yazıda anlattığım pattern’ları kendi projelerine uygularsan, hem geliştirme sürecin hızlanır hem de sağlam bir temel üzerine inşa etmiş olursun.
Bir sonraki adım olarak Deno Deploy’un WebSocket desteğini ve Cron Jobs özelliğini keşfetmeni öneririm. Edge’de realtime uygulama geliştirmek de aynı derecede eğlenceli bir konu.
