A school directory, an admissions app, or a facility map — all of them rest on the same query pattern.
Three reference datasets are available through the same API: 215,333 schools (from Dapodik), 4,759 higher-education institutions (PDDikti), and 3,116 hospitals (SIRS Kemenkes). The pattern is identical, so what you learn on one dataset transfers directly to the other two.
Filter, do not scroll
With 215,000 rows, showing "all schools" is meaningless. Design your UI around filters from the start — and the API provides exactly the filters you need:
# private vocational schools in one regency GET /schools?bentuk=SMK&status=S®ency=3204 # search by name GET /schools?q=SMA Negeri 1 # direct lookup by NPSN GET /schools?npsn=20106343
Filters combine freely, and every response carries total — that number drives your pagination and tells the user how broad their result set is.
The response shape
{
"status": "success",
"total": 749,
"page": 1,
"per_page": 100,
"total_pages": 8,
"count": 100,
"source": "Dapodik/Kemdikbud",
"data": [
{
"npsn": "20106343",
"name": "SMA NEGERI 1 CIAWIGEBANG",
"bentuk": "SMA",
"status": "N",
"province": "Jawa Barat",
"regency": "Kab. Kuningan",
"district": "Ciawigebang",
"address": "Jl. Siliwangi No. 1",
"lat": -6.974,
"lng": 108.5894
}
]
}Coordinates unlock map features
Most school rows carry lat and lng, which turns a text directory into a map. Some rows are null — Dapodik data is not complete for every school — so always handle that case rather than assuming coordinates exist:
const res = await fetch('/api/proxy/schools?bentuk=SMA®ency=3204&per_page=500');
const { data } = await res.json();
data
.filter((s) => s.lat !== null && s.lng !== null) // skip rows without coords
.forEach((s) => {
L.marker([s.lat, s.lng])
.bindPopup(`<strong>${s.name}</strong><br>${s.address ?? ''}`)
.addTo(map);
});Paginate without making users wait
The per_page ceiling is 500. For long lists, fetch the first page immediately and load the rest in the background — the user sees results at once instead of staring at a spinner.
async function fetchAll(query) {
const first = await get(`${query}&page=1&per_page=500`);
render(first.data); // show immediately
const rest = [];
for (let p = 2; p <= first.total_pages; p++) {
rest.push(get(`${query}&page=${p}&per_page=500`));
}
const pages = await Promise.all(rest); // load in parallel
pages.forEach((pg) => render(pg.data));
}Universities and hospitals
The other two datasets use the same query pattern, with filters suited to their domain:
# state polytechnics GET /universities?q=politeknik negeri # psychiatric hospitals in West Java GET /hospitals?type=RS Jiwa&province=32
Ready to build?
Create a free account and get your API key — 5,000 free hits a month, and two endpoints that need no key at all.