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.
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:
- Tarayıcı yalnız kendi sunucunuzdaki
/api/adres/...yollarını çağırır. - Sunucunuz isteği denetler, anahtarı ekler ve API'ye iletir.
- 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.
| Seviye | Uç (örnek kodla) | Sayfalama | Önbellek |
|---|---|---|---|
| İl | GET /v1/provinces | Yok, tüm liste tek yanıtta | Evet |
| İlçe | GET /v1/provinces/34/districts | Yok, tüm liste tek yanıtta | Evet |
| Mahalle | GET /v1/districts/1352/neighborhoods | Yok, tüm liste tek yanıtta | Evet |
| Cadde/sokak | GET /v1/neighborhoods/20239/streets | limit ve cursor | Hayır |
| Dış kapı | GET /v1/streets/173405/buildings | limit, cursor, q | Hayı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:
codeyalnı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
Mapyerine paylaşılan bir önbellek kullanın. - Hata aktarımı: Durum kodu ve
error.codetarayıcıya aynen geçer;429yanıtındakiRetry-Afterbaşlığı korunur. Arayüz kararlarınımessagemetnine 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 === codekontrolü 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.namealanı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ıttakimeta.nextCursordeğerini bir sonraki isteğe ekler ve değernullolduğ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çinGET /v1/autocompleteucunutypes=streetveneighborhoodCodeparametreleriyle 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.nextCursornullolana kadar ilerleniyor. - Veritabanında
stableIdsaklanıyor; kapıiddeğeri kalıcı referans olarak kullanılmıyor. 401ve403hatalarında otomatik tekrar yok;429yanıtındaRetry-Afterbekleniyor;503 catalog_unavailableiç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.