CityKit: The Zero-Dependency npm Package That Makes City Data Easy in Node.js
Amit Kumar Raikwar
Lead Strategist

We built CityKit because we were tired of installing heavyweight geo libraries just to answer simple questions like 'what is the nearest city to these coordinates?' Here is how it works and why it is now our go-to tool for any project that touches location data.
Why We Built This
Last year, we were building a feature that needed to show users the three nearest major cities to their GPS coordinates. Simple enough, right? We looked at the available libraries and found two categories: massive geospatial toolkits that brought in 12 dependencies and needed half a megabyte of native bindings just to compile, or tiny helpers that only worked for one country. Neither fit.
So we built CityKit. The idea was straightforward: ship a well-structured city dataset alongside a small set of hand-written utility functions, keep the whole thing in pure TypeScript, and add zero dependencies. The result is @novaedgedigitallabs/citykit, now publicly available on npm.
Getting Started in 30 Seconds
Installation is a single command. Once it is in your project, you pick between two entry points depending on how much data you actually need.
npm install @novaedgedigitallabs/citykitThe library ships with two datasets and two matching entry points. If you need comprehensive global coverage, use the Full dataset. If you are running in a serverless function where cold-start time and memory matter, the Lite dataset is the smarter pick.
// Full dataset — 49,992 cities worldwide
import { search, nearest, distance, fuzzySearch } from '@novaedgedigitallabs/citykit';
// Lite dataset — 1,422 major cities (great for serverless / edge)
import { search, nearest, distance, fuzzySearch } from '@novaedgedigitallabs/citykit/lite';The Four Functions You Will Use Most
CityKit ships with 12+ utilities but there are four that cover 90% of real-world use cases. Let me walk through each one with a concrete example.
1. search() — Exact Name Lookup
The search() function does a case-insensitive match against city names. It accepts an optional filter object so you can narrow results by country code, continent, or minimum population. This is the function to reach for when a user types a city name into a form.
import { search } from '@novaedgedigitallabs/citykit';
// Find all cities named 'Paris'
const allParis = search('paris');
console.log(allParis.length); // Returns Paris (France), Paris (Texas), and others
// Narrow to France only
const parisFrance = search('paris', { country: 'FR' });
console.log(parisFrance[0].name); // 'Paris'
console.log(parisFrance[0].population); // 21385512. fuzzySearch() — Typo-Tolerant Lookup
This is the function that surprised us the most in practice. Users mistype city names constantly — 'Mumbay', 'Banglaore', 'Hydrabad'. The fuzzySearch() function uses the Levenshtein distance algorithm to rank cities by how close they are to what the user typed. It is O(n × m) where n is the dataset size and m is the query length, so use the Lite dataset for very high-concurrency scenarios.
import { fuzzySearch } from '@novaedgedigitallabs/citykit';
// Works even with typos
const results = fuzzySearch('Banglaore', { limit: 5 });
console.log(results[0].name); // 'Bangalore'
console.log(results[0].country); // 'IN'
// Each result includes a similarity score between 0 and 1
console.log(results[0].score); // 0.893. nearest() — Closest City to Coordinates
Give it a latitude and longitude, and nearest() returns the closest cities sorted by distance. This is the function we originally built the whole library for. Under the hood, it calculates Haversine distance against the entire dataset, so it scans all rows — but for typical use cases like mobile apps or API endpoints, the performance is more than acceptable.
import { nearest } from '@novaedgedigitallabs/citykit';
// User's GPS coordinates
const userLat = 22.7196;
const userLng = 75.8577;
// Get the 3 nearest cities
const closestCities = nearest(userLat, userLng, { limit: 3 });
closestCities.forEach(city => {
console.log(`${city.name} — ${city.distanceKm.toFixed(1)} km away`);
});
// Indore — 1.2 km away
// Ujjain — 55.4 km away
// Bhopal — 189.3 km away4. distance() — Point-to-Point Measurement
The distance() function calculates the great-circle distance between two cities by name. It uses the Haversine formula and returns the result in both kilometers and miles. You can also call distanceByCoords() directly if you already have lat/lng values.
import { distance } from '@novaedgedigitallabs/citykit';
const result = distance('Mumbai', 'Delhi');
console.log(`Distance: ${result.km} km / ${result.miles} miles`);
// Distance: 1148.3 km / 713.4 milesFull vs. Lite: How to Pick the Right Dataset
This decision matters more than it might look. The Full dataset holds 49,992 cities and gives you complete global coverage — every small town, every suburb, every named settlement. The Lite dataset holds 1,422 major cities and is deliberately curated to include only population centers large enough to appear in everyday conversation.
The practical rule: use Full for backend services running on long-lived Node servers. Use Lite for Vercel Edge Functions, AWS Lambda, or any environment where startup time and memory are constrained. The Lite dataset loads faster and occupies a fraction of the heap, which directly cuts cold-start latency.
// In an AWS Lambda or Vercel Edge Function — use Lite
import { search, stats } from '@novaedgedigitallabs/citykit/lite';
const info = stats();
console.log(info.totalCities); // 1422
console.log(info.totalCountries); // 183The Utilities You Did Not Expect to Need
Beyond the four core functions, CityKit ships several helpers that handle common filtering tasks that would otherwise require multiple lines of manual code.
byCountry() returns all cities in a given country. byContinent() returns all cities on a given continent using built-in ISO continent mappings. withinRadius() returns all cities within a specific kilometer radius of a lat/lng point — useful for building delivery zone selectors or store locators. byPopulation() returns cities above a minimum population threshold, which is helpful for seeding a dropdown that should only show major cities.
import { byCountry, byContinent, withinRadius, byPopulation } from '@novaedgedigitallabs/citykit';
// All cities in Japan
const japanCities = byCountry('JP');
console.log(japanCities.length); // 1,243
// All cities in Europe
const europeCities = byContinent('Europe');
// Cities within 100km of a location
const nearby = withinRadius(28.6139, 77.2090, 100); // Near New Delhi
console.log(nearby.length); // ~47 cities
// Only cities with population over 1 million
const megaCities = byPopulation(1_000_000);
console.log(megaCities.length); // 548A Real-World Example: Building a City Autocomplete API
Here is a complete example of how you would wire CityKit into a Next.js API route to power a city search autocomplete. The endpoint accepts a query string, runs a fuzzy search, and returns the top five matches as JSON.
// app/api/city-search/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { fuzzySearch } from '@novaedgedigitallabs/citykit';
export async function GET(req: NextRequest) {
const query = req.nextUrl.searchParams.get('q');
if (!query || query.length < 2) {
return NextResponse.json({ results: [] });
}
const results = fuzzySearch(query, { limit: 5 });
const formatted = results.map(city => ({
id: `${city.name}-${city.countryCode}`,
label: `${city.name}, ${city.country}`,
lat: city.lat,
lng: city.lng,
population: city.population,
}));
return NextResponse.json({ results: formatted });
}
// GET /api/city-search?q=Banglaore
// Response:
// { results: [{ label: 'Bangalore, India', lat: 12.97, lng: 77.56, ... }] }Honest Limitations to Know About
We try to be direct about what the library does not do well.
First, nearest() and fuzzySearch() perform a full linear scan of the dataset on every call. For a server handling hundreds of concurrent requests, this is fine. For a hot API route receiving tens of thousands of requests per second, you would want to add a caching layer or implement a spatial index. We are tracking a k-d tree implementation for a future release.
Second, data is loaded synchronously at startup using readFileSync. This means the first import adds a small blocking cost. On a typical Node.js server this is invisible, but it is worth knowing if you are measuring cold-start timing for serverless deployments.
Third, there is no browser build. The library uses fs and path modules, which are Node.js-specific. If you need browser-compatible geolocation, CityKit is not the right tool. A future version may ship a bundled browser build, but that is not on the current roadmap.
Try It Now
CityKit is open source and available on npm right now. The package page at npmjs.com/package/@novaedgedigitallabs/citykit has the full API reference, and the source code is on GitHub under the NovaEdge Digital Labs organization. Pull requests and issue reports are welcome.
If you build something interesting with it — a store locator, a time zone picker, a travel app — we would genuinely love to hear about it. Drop us a message through the NovaEdge contact page or open an issue on the GitHub repository.
Frequently Asked Questions

About Amit Kumar Raikwar
NovaEdge Digital Labs is a team of designers, developers, and strategists dedicated to pushing the boundaries of digital innovation in 2026.
Learn more about the team