Back to Insights/Open Source

CityKit: The Zero-Dependency npm Package That Makes City Data Easy in Node.js

NovaEdge Logo

Amit Kumar Raikwar

Lead Strategist

June 11, 20268 min read
CityKit: The Zero-Dependency npm Package That Makes City Data Easy in Node.js

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.

bash@novaedgedigitallabs/citykit
npm install @novaedgedigitallabs/citykit

The 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.

typescript@novaedgedigitallabs/citykit
// 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.

typescript@novaedgedigitallabs/citykit
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); // 2138551

2. 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.

typescript@novaedgedigitallabs/citykit
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.89

3. 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.

typescript@novaedgedigitallabs/citykit
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 away

4. 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.

typescript@novaedgedigitallabs/citykit
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 miles

Full 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.

typescript@novaedgedigitallabs/citykit
// 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); // 183

The 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.

typescript@novaedgedigitallabs/citykit
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); // 548

A 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.

typescript@novaedgedigitallabs/citykit
// 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

#npm#Node.js#TypeScript#Open Source#Geolocation#City Data
NovaEdge Logo

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

Keep Reading

Related Insights