Blog · Geliştirici rehberi

İl, İlçe ve Mahalle Seçimli Adres Formu Nasıl Kurulur?

Kademeli seçim kutularıyla il, ilçe, mahalle, sokak ve dış kapı seçilen bir adres formunu önbellek, stableId ve sunucu proxy'siyle adım adım kurun.

4 dk okuma

Serbest metinle alınan adresler, aynı yerin farklı biçimlerde yazılmasına açıktır: "Caferağa Mah.", "caferaga mahallesi", "Cafer Ağa Mh." gibi. Kademeli seçim kutularında kullanıcı adresi yazmaz, katalogdan seçer. Her kutu bir öncekine göre daralır ve kayıt, kimlikleriyle birlikte sisteminize girer.

Bu yazıda il → ilçe → mahalle → cadde/sokak → dış kapı sırasıyla çalışan böyle bir formu küçük bir Node.js proxy'si ve sade bir tarayıcı betiğiyle adım adım kuracağız.

Mimari: anahtar sunucuda, form tarayıcıda

API Türkiye'ye yapılan her istek X-API-Key başlığını taşır. Anahtar tarayıcı koduna girerse geliştirici araçlarından okunabilir ve hesabınız adına başkaları istek atabilir. Bu yüzden akışı üç katmana ayırıyoruz:

  1. Tarayıcı yalnız kendi sunucunuzdaki /api/adres/... yollarını çağırır.
  2. Sunucunuz isteği denetler, anahtarı ekler ve API'ye iletir.
  3. Sık istenen il, ilçe ve mahalle listeleri sunucudaki önbellekten döner.

Her seviyede, bir sonraki listeyi istemek için seçilen kaydın code değeri kullanılır. Tablodaki sayılar yalnız örnektir; uygulamanızda bir üst listeden gelen kodu kullanın.

SeviyeUç (örnek kodla)SayfalamaÖnbellek
İlGET /v1/provincesYok, tüm liste tek yanıttaEvet
İlçeGET /v1/provinces/34/districtsYok, tüm liste tek yanıttaEvet
MahalleGET /v1/districts/1352/neighborhoodsYok, tüm liste tek yanıttaEvet
Cadde/sokakGET /v1/neighborhoods/20239/streetslimit ve cursorHayır
Dış kapıGET /v1/streets/173405/buildingslimit, cursor, qHayır

Koda geçmeden önce anahtarınızı terminalden deneyebilirsiniz:

curl "https://api.apiturkiye.com/v1/provinces/34/districts" -H "X-API-Key: apt_live_..."

Adım 1: Anahtarı gizleyen sunucu tarafı proxy

Aşağıdaki Express sunucusu, tarayıcıdan gelen seviye adını sabit bir uç listesiyle eşler. Tarayıcının gönderdiği yolu olduğu gibi API'ye iletmez; böylece proxy'niz her adresi çağırabilen açık bir aracıya dönüşmez. Seviye adı dönen listenin türünü belirtir: /api/adres/districts/34, 34 kodlu ilin ilçelerini getirir. Anahtar APITURKIYE_KEY ortam değişkeninden okunur.

// server.js — Node.js 18+, Express, ESM
import express from "express";

const API = "https://api.apiturkiye.com/";
const ROUTES = {
  provinces: () => "provinces",
  districts: (c) => `provinces/${c}/districts`,
  neighborhoods: (c) => `districts/${c}/neighborhoods`,
  streets: (c) => `neighborhoods/${c}/streets`,
  buildings: (c) => `streets/${c}/buildings`,
};
const PARAMS = { streets: ["limit", "cursor"], buildings: ["limit", "cursor", "q"] };
const CACHED = ["provinces", "districts", "neighborhoods"];
const cache = new Map();

async function callApi(level, code = "", query = {}) {
  if (level !== "provinces" && !/^\d{1,20}$/.test(code)) {
    return { status: 400, body: { error: { code: "invalid_code" } } };
  }
  const url = new URL(`v1/${ROUTES[level](code)}`, API);
  for (const p of PARAMS[level] ?? []) {
    if (typeof query[p] === "string") url.searchParams.set(p, query[p]);
  }
  const hit = cache.get(url.href);
  if (hit && hit.expires > Date.now()) return { status: 200, body: hit.body };

  const res = await fetch(url, { headers: { "X-API-Key": process.env.APITURKIYE_KEY } });
  const body = await res.json();
  if (res.ok && CACHED.includes(level)) {
    cache.set(url.href, { body, expires: Date.now() + 24 * 60 * 60 * 1000 });
  }
  return { status: res.status, body, retryAfter: res.headers.get("retry-after") };
}

const app = express();
app.use(express.json());

app.get(["/api/adres/:level", "/api/adres/:level/:code"], async (req, res) => {
  const { level, code } = req.params;
  if (!Object.hasOwn(ROUTES, level)) return res.status(404).json({ error: { code: "unknown_level" } });
  try {
    const out = await callApi(level, code, req.query);
    if (out.retryAfter) res.set("Retry-After", out.retryAfter);
    res.status(out.status).json(out.body);
  } catch {
    res.status(502).json({ error: { code: "upstream_unavailable" } });
  }
});

app.listen(3000);

Bu kodda üç noktaya dikkat edin:

  • Kod denetimi: code yalnız rakamlardan oluşuyorsa yola eklenir. Bu, yol manipülasyonu riskini azaltır ve bozuk istekleri API'ye hiç göndermez.
  • Önbellek: Yalnız başarılı il, ilçe ve mahalle yanıtları saklanır. Bu üç uç ilgili kapsamın tamamını tek yanıtta döndürür; mahalle listesini istemci tarafında ilk 100 kayıtla kesmeyin. Yirmi dört saatlik süre bir örnektir; katalog güncellemelerini makul sürede yansıtacak bir değer seçin. Birden fazla sunucu örneği çalıştırıyorsanız bellek içi Map yerine paylaşılan bir önbellek kullanın.
  • Hata aktarımı: Durum kodu ve error.code tarayıcıya aynen geçer; 429 yanıtındaki Retry-After başlığı korunur. Arayüz kararlarını message metnine değil, hata koduna göre verin.

Adım 2: Her seçimde bir alt seviyeyi yüklemek

Formda il, ilce, mahalle, sokak ve kapi kimlikli beş seçim kutusu olduğunu, il dışındakilerin başlangıçta devre dışı olduğunu varsayalım. Kural basit: bir kutu değiştiğinde altındaki tüm kutular boşaltılır ve yalnız bir alt seviye yüklenir. Kullanıcı ilçe seçmeden mahalle listesi istenmez.

// adres-formu.js — API anahtarı içermez
const LEVELS = ["il", "ilce", "mahalle", "sokak", "kapi"];
const el = Object.fromEntries(LEVELS.map((id) => [id, document.getElementById(id)]));

async function getJson(url) {
  const res = await fetch(url);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json();
}
const list = async (path) => (await getJson(path)).data;

async function loadAll(path) {
  const items = [];
  let cursor = null;
  do {
    const url = new URL(path, location.origin);
    url.searchParams.set("limit", "100");
    if (cursor) url.searchParams.set("cursor", cursor);
    const body = await getJson(url);
    items.push(...body.data);
    cursor = body.meta.nextCursor;
  } while (cursor);
  return items;
}

function fill(select, items, labelOf) {
  select.replaceChildren(new Option("Seçiniz", ""));
  for (const item of items) {
    const opt = new Option(labelOf(item), item.code);
    opt.dataset.stableId = item.stableId;
    opt.dataset.name = item.name ?? item.doorNumber;
    if (item.id) opt.dataset.doorId = item.id;
    select.add(opt);
  }
  select.disabled = items.length === 0;
}

function bind(parentId, load, labelOf) {
  const parent = el[parentId];
  const below = LEVELS.slice(LEVELS.indexOf(parentId) + 1);
  parent.addEventListener("change", async () => {
    below.forEach((id) => fill(el[id], []));
    const code = parent.value;
    if (!code) return;
    try {
      const items = await load(code);
      if (parent.value === code) fill(el[below[0]], items, labelOf);
    } catch {
      el[below[0]].replaceChildren(new Option("Liste yüklenemedi", ""));
    }
  });
}

bind("il", (c) => list(`/api/adres/districts/${c}`), (d) => d.name);
bind("ilce", (c) => list(`/api/adres/neighborhoods/${c}`),
  (n) => (n.locality?.village ? `${n.name} (${n.locality.village.name})` : n.name));
bind("mahalle", (c) => loadAll(`/api/adres/streets/${c}`), (s) => s.name);
bind("sokak", (c) => loadAll(`/api/adres/buildings/${c}`),
  (k) => (k.label ? `${k.doorNumber} (${k.label})` : k.doorNumber));
list("/api/adres/provinces").then((items) => fill(el.il, items, (p) => p.name));

Betikteki önemli ayrıntılar:

  • Geç gelen yanıtlar: Kullanıcı seçimi hızla değiştirirse önceki isteğin yanıtı sonra gelebilir. parent.value === code kontrolü eski listenin yeni seçimin altına yazılmasını engeller.
  • Mahalle etiketi: Aynı mahalle adı bir ilçenin farklı yerleşimlerinde bulunabilir. Mahalle listesindeki locality.village.name alanını etikete eklemek, kullanıcının doğru kaydı ayırt etmesine yardım eder. Bu alanı bir yerleşim sınıflandırması olarak yorumlamayın; yalnız ayırt etmeye yarayan bir etikettir.
  • Sayfalı listeler: Sokak ve kapı uçları cursor ile sayfalanır. loadAll, yanıttaki meta.nextCursor değerini bir sonraki isteğe ekler ve değer null olduğunda durur.
  • Uzun listeler: Kapı sayısı fazla olan bir sokakta tüm listeyi doldurmak yerine bir metin kutusu koyup numara önekine göre arayabilirsiniz: /api/adres/buildings/173405?q=24. Sokaklar için GET /v1/autocomplete ucunu types=street ve neighborhoodCode parametreleriyle kullanarak bir arama kutusu kurabilirsiniz; sorgu en az 3 karakter olmalıdır ve bu ucu proxy'nize ayrıca eklemeniz gerekir.

Hazır bir başlangıç noktası isterseniz adres seçici bileşen örneklerine göz atabilirsiniz.

Adım 3: Seçilen kayıtların stableId değerini saklamak

Form gönderildiğinde yalnız ekranda görünen metni değil, her seviyenin kimliklerini de sunucuya gönderin. Yanıtlardaki üç kimlik alanının görevi farklıdır:

  • stableId: Sürümden bağımsız, kalıcı kimlik. Veritabanında adres referansı olarak bunu saklayın.
  • code: Kaynak kodu. Alt seviyeleri istemek ve başka sistemlerle eşleştirme yapmak için korunur.
  • id: Yalnız dış kapılarda bulunur, aktif katalog sürümüne aittir ve sürümler arasında değişebilir. Kalıcı referans olarak kullanmayın.

Aşağıdaki kodu aynı betiğe ekleyin (formun kimliği adres-formu). Her kutunun seçili seçeneğinden { code, stableId, name } nesnesi üretilir; kapı nesnesinde ayrıca doorId bulunur:

const pick = (id) => {
  const opt = el[id].selectedOptions[0];
  return opt?.value ? { code: opt.value, ...opt.dataset } : null;
};

document.getElementById("adres-formu").addEventListener("submit", async (e) => {
  e.preventDefault();
  const address = Object.fromEntries(LEVELS.map((id) => [id, pick(id)]));
  if (Object.values(address).includes(null)) return alert("Lütfen tüm alanları seçin.");
  await fetch("/api/adresler", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(address),
  });
});

Veritabanında her seviye için stableId ve code değerlerini, yanında da kullanıcının o anda gördüğü adları tutun. Kimlik kaydın katalogla ilişkisini korur; ad ise eski siparişlerin veya başvuruların okunabilir kalmasını sağlar.

Adım 4: Sunucuda son kontrol

Seçeneklerin data- öznitelikleri tarayıcıda kolayca değiştirilebilir; gelen veriye doğrudan güvenmeyin. Önbellekteki listelerle gelen ilçenin gerçekten seçilen ilin altında olduğunu, mahalle için de aynı şeyi kontrol edebilirsiniz. Önbellek dolu olduğunda bu kontrol API'ye ek istek atmaz. Ağ hatalarına karşı bloğu Adım 1'deki gibi try/catch içine alın:

// POST /api/adresler işleyicisinde
const { il, ilce } = req.body ?? {};
const { status, body } = await callApi("districts", il?.code);
const ok = status === 200 && body.data.some((d) => d.stableId === ilce?.stableId);
if (!ok) return res.status(422).json({ error: { code: "address_mismatch" } });

Sokak ve kapı sayfalı listeler olduğu için aynı yöntemle denetlemek zahmetlidir. Pro ve üzeri paketlerde POST /v1/addresses/validate ucu bu işi üstlenir: seçilen adları ve kapı numarasını, aynı numaradaki kayıtları ayırmak için de kapının id değerini doorId olarak gönderirsiniz. Mahalle için etiketi değil, listeden gelen name değerini kullanın. Yanıtta data.valid true ise data.components içindeki stableId değerlerini formdan gelenlerle karşılaştırın; false ise failedAt alanı eşleşmenin hangi seviyede durduğunu gösterir. Bu uç aynı adlı mahalleler arasında kendiliğinden seçim yapmaz; bu yüzden karşılaştırmayı adla değil stableId ile yapın.

Yayına almadan önce kontrol listesi

  • Anahtar yalnız sunucudaki ortam değişkeninde; tarayıcı paketinde, Git deposunda ve loglarda yok.
  • Proxy yalnız tanımlı seviyeleri ve rakamlardan oluşan kodları kabul ediyor.
  • İl, ilçe ve mahalle listeleri önbellekten dönüyor; mahalle listesi kesilmeden gösteriliyor.
  • Üst seviye değişince alt kutular boşalıyor, geç gelen yanıtlar yok sayılıyor.
  • Sokak ve kapı listelerinde meta.nextCursor null olana kadar ilerleniyor.
  • Veritabanında stableId saklanıyor; kapı id değeri kalıcı referans olarak kullanılmıyor.
  • 401 ve 403 hatalarında otomatik tekrar yok; 429 yanıtında Retry-After bekleniyor; 503 catalog_unavailable için kullanıcıya anlaşılır bir mesaj gösteriliyor.

Uçların tüm parametrelerini ve hata kodlarını API dokümantasyonunda bulabilirsiniz. Henüz anahtarınız yoksa ücretsiz kayıt olup formu kendi projenizde deneyebilir, doğrulama ucuna ihtiyaç duyduğunuzda paketleri karşılaştırabilirsiniz.