roedyrustam/api-wilayah-2024
Laravel package for Indonesia’s 2024 administrative region data, providing provinces, regencies, districts, and villages with ready-to-use API routes and search for location pickers, address forms, and regional lookups.
Install the package and let Laravel's package discovery register the service provider:
composer require roedyrustam/api-wilayah-2024
Publish whatever the package ships (config, migrations, seeders, and—if it exposes HTTP routes—route files). If you're not sure what the provider is named, run php artisan vendor:publish with no arguments and pick the api-wilayah entry from the list; the README will confirm the exact tag.
php artisan vendor:publish --tag=wilayah-config
php artisan vendor:publish --tag=wilayah-migrations # or the tag shown in the README
php artisan migrate
The dataset is large (roughly 91k rows: 38 provinces, 514 kab/kota, 7,277 kecamatan, 83,763 kelurahan/desa), so seeding is the first thing that will surprise you. Prefer the package's artisan import command if one is provided (e.g. php artisan wilayah:import) over db:seed, and give PHP a generous memory_limit the first time.
Look first at these files after publishing:
config/wilayah.php — table names, connection, and any route prefix settingsdatabase/migrations/*wilayah* — confirm the schema matches your existing address tablesroutes/ — whether it auto-registers JSON endpoints for cascading dropdownsA quick sanity check:
// php artisan tinker
\RoedyRustam\ApiWilayah\Models\Provinsi::where('kode', '31')->first(); // DKI Jakarta
\RoedyRustam\ApiWilayah\Models\Provinsi::count(); // 38
First use case: an address/checkout form. Drop three or four cascading <select> elements on the page, populate provinces from the package models, then fetch kab/kota, kecamatan, and kelurahan on change via small controller endpoints (or the package's own routes if present). Store the 10-digit BPS kode on your addresses table, not the auto-increment ID — the code is stable across reseeds and is what shipping carriers and Kemendagri-aligned systems expect.
Use kode as the foreign key, everywhere. The package follows the BPS/Kemendagri coding scheme (2 digits province, 4 kab/kota, 6 kecamatan, 10 kelurahan/desa, hierarchical prefixes). Storing kode on your own addresses, users, or orders tables makes the data portable and lets you rebuild the wilayah tables without cascading ID breakage.
Schema::table('addresses', function (Blueprint $t) {
$t->char(2, 10)->nullable()->index(); // provinsi_kode
$t->char(4, 10)->nullable()->index(); // kabupaten_kode
$t->char(6, 10)->nullable()->index(); // kecamatan_kode
$t->char(10, 10)->nullable()->index(); // kelurahan_kode
});
Cache the lookup layers. Provinces and kab/kota are small and effectively immutable between releases—memoize them forever and bust on redeploy:
$provinces = Cache::rememberForever('wilayah.provinsi', fn () =>
Provinsi::orderBy('nama')->get(['kode', 'nama'])
);
Do the same for kab/kota keyed by parent, but skip caching the full 83k kelurahan list; query it on demand or cache per-kecamatan.
Cascading endpoints with route-model binding by code. If the package doesn't ship routes, write thin controllers:
Route::get('/wilayah/kabupaten/{provinsi}', fn (string $provinsi) =>
Kabupaten::where('provinsi_kode', $provinsi)->orderBy('nama')->get(['kode', 'nama'])
);
Then wire Alpine or Livewire to fetch() on change. Keep responses to kode and nama only—serializing full models for 500+ rows per request is wasteful.
Validation. The dataset gives you free exists rules; don't hand-roll arrays:
$request->validate([
'provinsi_kode' => ['required', Rule::exists('wilayah_provinsi', 'kode')],
'kabupaten_kode' => ['required', Rule::exists('wilayah_kabupaten', 'kode')
->where('provinsi_kode', $request->provinsi_kode)],
// ...
]);
The chained where clauses enforce hierarchy, which catches tampered form posts.
Tests. 90k rows will slow your suite to a crawl if you re-seed on every test. Seed once into a shared test database, or wrap your test class in a transaction and rely on RefreshDatabase's $seed = false. For unit tests of unrelated features, point the package at a SQLite connection you seed in a setUpBeforeClass.
Read-only separation. If the wilayah tables don't belong in your application's primary schema, publish the config and point them at a second connection (a read replica or a dedicated wilayah SQLite file). The package is data-only, so there's no reason to let it bloat your main migration history.
->chunk(1000) or raw insert() batches of 500–1000), wrap the whole thing in DB::transaction(), and disable foreign key checks (Schema::disableForeignKeyConstraints()) around the load. Without this, 91k Eloquent create() calls will time out.11.01.01.2001), some bare digits. Confirm which the package stores and normalize at your boundary—mix them and your joins silently return nothing.nama; always key by kode, and for search UI, show the parent chain in the label ("Sukamaju, Cilodong, Depok").nama column typically carries the full official name including the "Kabupaten"/"Kota" prefix. Don't strip prefixes for display without also handling filtering, or users searching "Kota Bandung" miss it.provinces/regencies tables from another package, change the package's table names in config/wilayah.php before running the migration, or you'll get duplicate-table errors./api/wilayah/* globally, it may collide with your own prefix or leak data outside auth middleware. Disable auto-routes in config and mount them yourself if you need auth/rate limiting.kode disappeared (pemekaran/mergers do happen). Log orphaned codes rather than letting them fail silently.Cache::rememberForever keys you added will survive the reseed. Either version the cache key (wilayah.provinsi.v2026) or flush explicitly in a deploy command."roedyrustam/api-wilayah-2024": "1.x") rather than floating, read the migration files before trusting them in production, and consider vendoring the seed data (export to CSV/JSON) so a future breaking change or abandonment doesn't strand you. The data itself is the valuable part—your application shouldn't depend heavily on the package's PHP surface.Http::fake() are fragile because the response shape is the dataset. Prefer hitting the real seeded database in feature tests and reserve HTTP fakes for external services.How can I help you explore Laravel packages today?