This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
GeoIP2-node is MaxMind's official Node.js/TypeScript client library for:
- GeoIP2/GeoLite2 Web Services: Country, City, and Insights endpoints
- GeoIP2/GeoLite2 Databases: Local MMDB file reading for various database types (City, Country, ASN, Anonymous IP, Anonymous Plus, ISP, etc.)
The library provides both web service clients and database readers that return strongly-typed model objects containing geographic, ISP, anonymizer, and other IP-related data.
Key Technologies:
- TypeScript with strict type checking
- Node.js 18+ (targets active LTS versions)
- maxmind (node-maxmind) for MMDB database reading
- Jest for testing
- ESLint + Prettier for code quality
- TypeDoc for API documentation
src/
├── models/ # Response models (City, Country, AnonymousIP, etc.)
├── records.ts # TypeScript interfaces for data records
├── reader.ts # Database file reader
├── readerModel.ts # Database lookup methods
├── webServiceClient.ts # HTTP client for MaxMind web services
├── utils.ts # Utility functions (camelCase conversion, CIDR)
├── errors.ts # Custom error classes
└── types.ts # Type definitions
API responses and database data use snake_case, but the library exposes camelCase properties:
// API returns: { network_last_seen: "2025-04-14" }
// Model exposes: anonymousPlus.networkLastSeen
constructor(response: mmdb.AnonymousPlusResponse) {
this.networkLastSeen = response.network_last_seen;
}The camelcaseKeys() utility in utils.ts handles deep conversion for web service responses. Database models handle conversion in constructors.
Models follow clear inheritance patterns:
Country→ base model with country/continent/traits dataCityextendsCountry→ adds city, location, postal, subdivisionsInsightsextendsCity→ adds web service-specific fieldsEnterpriseextendsCity→ adds enterprise database fields
Models are constructed from raw response data with optional IP address and network:
public constructor(
response: ResponseType,
ipAddress?: string,
network?: string
) {
// Handle data transformation
this.traits.ipAddress ??= ipAddress;
this.traits.network ??= network;
}Boolean traits are normalized to false when missing (never undefined):
private setBooleanTraits(traits: any) {
const booleanTraits = [
'isAnonymous',
'isAnonymousProxy',
// ...
];
booleanTraits.forEach((trait) => {
traits[trait] = !!traits[trait];
});
return traits as records.TraitsRecord;
}Database access uses a two-step pattern:
Reader.open(path)orReader.openBuffer(buffer)→ returnsReaderModelreaderModel.city(ip)/readerModel.anonymousPlus(ip)→ returns model
The ReaderModel wraps the underlying node-maxmind reader and provides typed lookup methods.
Web service access uses direct methods on WebServiceClient:
const client = new WebServiceClient(accountID, licenseKey, { host, timeout });
const response = await client.city('1.2.3.4');# Install dependencies
npm install
# Run all tests
npm test
# Run tests in watch mode
npm run test:watch
# Run specific test file
npx jest src/readerModel.spec.ts# Lint code (ESLint)
npm run lint
# Format code (Prettier)
npm run prettier:ts
# Build TypeScript
npm run build
# Build and deploy documentation
npm run build:docs
npm run deploy:docsTests use .spec.ts files co-located with source:
src/readerModel.spec.ts- Database reader testssrc/webServiceClient.spec.ts- Web service client testssrc/reader.spec.ts- Reader initialization testssrc/utils.spec.ts- Utility function tests
Additional integration tests in test/ and e2e/ directories.
When adding new fields to models:
- Update test fixtures/mocks to include the new field
- Add assertions to verify the field is properly mapped
- Test both presence and absence (undefined handling)
- Verify camelCase conversion if from snake_case source
Example:
const response = {
anonymizer_confidence: 99,
network_last_seen: '2025-04-14',
provider_name: 'FooBar VPN',
};
const model = new AnonymousPlus(response, '1.2.3.4');
expect(model.anonymizerConfidence).toBe(99);
expect(model.networkLastSeen).toBe('2025-04-14');
expect(model.providerName).toBe('FooBar VPN');-
Add the property with proper type annotation:
/** * Description of the field, including availability (which endpoints/databases). */ public fieldName?: Type;
-
Update the constructor to map from the response:
this.fieldName = response.field_name;
-
For Country/City models using camelcaseKeys, the conversion is automatic. For other models, handle snake_case explicitly.
-
Update corresponding record interfaces in
records.tsif the field appears in a reusable record type (likeTraitsRecord). -
Add tests that verify the field mapping and type.
-
Update CHANGELOG.md with the change (see format below).
When creating a new model class:
- Determine the appropriate base class (standalone, extends Country, extends City)
- Create the model file in
src/models/YourModel.ts - Export from
src/models/index.ts - Follow the constructor pattern with response, ipAddress, network parameters
- Add corresponding method in
readerModel.tsorwebServiceClient.ts - Update type definitions in
types.tsif needed - Add comprehensive tests
- Update TypeDoc comments for generated API documentation
Always update CHANGELOG.md for user-facing changes.
Important: Do not add a date to changelog entries until release time.
- If the next version doesn't exist, create it as
X.Y.Z (unreleased) - If it already exists without a date (e.g.,
6.3.0 (unreleased)), add your changes there - The release date will be added when the version is actually released
6.3.0 (unreleased)
------------------
* A new `fieldName` property has been added to `ModelName`. This field
provides information about... Available from [endpoint/database names].
* The `oldField` property in `ModelName` has been deprecated. Please use
`newField` instead.Database responses use snake_case but must be exposed as camelCase.
Solution:
- For
CountryandCitymodels:camelcaseKeys()handles this automatically - For other models (e.g.,
AnonymousPlus): Manually map in constructorthis.networkLastSeen = response.network_last_seen;
Boolean traits should always be false when missing, never undefined.
Solution: Use the setBooleanTraits() pattern or explicit !! coercion:
this.isAnonymous = !!response.is_anonymous;Models should include the IP address and network from lookups.
Solution: Always pass and set these optional parameters:
this.traits.ipAddress ??= ipAddress;
this.traits.network ??= network;The underlying maxmind package has its own types.
Solution: Import types from maxmind when needed:
import * as mmdb from 'maxmind';
public constructor(response: mmdb.AnonymousPlusResponse) {
// ...
}- TypeScript strict mode - All files use strict type checking
- ESLint - Configured with TypeScript ESLint rules (see
eslint.config.mjs) - Prettier - Consistent formatting enforced
- Prefer arrow callbacks - Use arrow functions for callbacks
- await-thenable - Only await promises
- No unused variables/imports - Clean up unused code
- TypeDoc comments - Document public APIs with JSDoc-style comments
npm install# Tidy code (auto-fix issues)
precious tidy -g
# Lint code (check for issues)
precious lint -g
# Run tests
npm test
# Build
npm run buildNote: Precious is already set up and handles code formatting and linting. Use precious tidy -g to automatically fix issues, and precious lint -g to check for remaining problems.
- Node.js 18+ required (targets active LTS: 18, 20, 22)
- Uses Node.js built-in
fetch(no external HTTP libraries) - TypeScript 5.x
This library is part of MaxMind's multi-language client library ecosystem. When adding features:
- Field names should match other client libraries (PHP, Python, etc.) after camelCase conversion
- Model structure should parallel other implementations where possible
- Error handling patterns should be consistent
- Documentation style should follow established patterns
Refer to the GeoIP2-php implementation for guidance on new features (especially model/record additions).