JavaScript / TypeScript
Server (Node & edge)
@ipgeotrace/client is the zero-dependency, fetch-based core: resolve any IP you supply, single or in batches, with caching and retries. Framework adapters build on it to resolve the caller automatically, once per request.
The core runs anywhere fetch exists - Node 18+, edge runtimes, Deno and Bun - and holds your secret key. You supply the IP; the framework adapters below take care of pulling it off the request for you.
Create the client#
One client, constructed with your secret key. Keep it server-side and reuse it across requests.
import { IpGeoTraceClient } from '@ipgeotrace/client';
const geo = new IpGeoTraceClient({ apiKey: process.env.IPGEOTRACE_API_KEY! });Resolve a single address#
resolve returns a Result you check with ok before reading value. Here it screens new signups for datacenter IPs and country mismatches.
async function needsReview(signupIp: string, billingCountry: string): Promise<boolean> {
const result = await geo.resolve(signupIp);
if (!result.ok) return false;
const fromDatacenter = result.value.asn?.type === 'hosting';
const countryMismatch =
result.value.country?.code != null && result.value.country.code !== billingCountry;
return fromDatacenter || countryMismatch;
}Resolve a batch#
resolveBatch enriches up to 100 addresses in one round trip - ideal for a page of audit records or login history. Cached addresses are served locally and only the misses hit the API.
const batch = await geo.resolveBatch(logins.map((l) => l.ip));
if (batch.ok) {
for (const item of batch.value.results) {
if (item.found) report.add(item.ip, item.country?.name, item.asn?.name);
}
}The GeoLookup status model#
Every adapter exposes the caller's location as a GeoLookup - the same four-state model as the .NET client, re-exported from the core so it can never drift. Branch on status:
resolved-valuecarries the data.skipped- the address was missing, invalid, private, link-local or loopback, so no API call was made.failed-errorcarries the reason, such asrate_limited.not_attempted- the middleware did not run orshouldResolvereturned false.
Framework middleware#
Each adapter resolves the caller once per request and exposes the GeoLookup where that framework expects it. Private, link-local and loopback addresses are detected locally and skipped without an API call or quota usage.
Express
app.use(ipgeotrace(...)) populates req.geo; read it directly or with getGeo(req).
import express from 'express';
import { ipgeotrace, getGeo } from '@ipgeotrace/express';
const app = express();
app.set('trust proxy', true);
app.use(ipgeotrace({ apiKey: process.env.IPGEOTRACE_API_KEY! }));
app.get('/pricing', (req, res) => {
const visitor = getGeo(req); // also on req.geo
const currency =
visitor.status === 'resolved' ? visitor.value.country?.currency ?? 'USD' : 'USD';
res.json(catalog.pricedIn(currency));
});Fastify
Register the plugin app-wide; it decorates every request with geo.
import Fastify from 'fastify';
import { ipgeotrace, getGeo } from '@ipgeotrace/fastify';
const app = Fastify({ trustProxy: true });
await app.register(ipgeotrace, { apiKey: process.env.IPGEOTRACE_API_KEY! });
app.get('/pricing', (request) => {
const visitor = getGeo(request); // also on request.geo
return catalog.pricedIn(
visitor.status === 'resolved' ? visitor.value.country?.currency ?? 'USD' : 'USD',
);
});NestJS
IpGeoTraceModule.forRoot registers a global interceptor; the @Geo() parameter decorator injects the lookup into any handler.
import { Module } from '@nestjs/common';
import { IpGeoTraceModule } from '@ipgeotrace/nestjs';
@Module({
imports: [IpGeoTraceModule.forRoot({ apiKey: process.env.IPGEOTRACE_API_KEY! })],
})
export class AppModule {}import { Controller, Get } from '@nestjs/common';
import { Geo, type GeoLookup } from '@ipgeotrace/nestjs';
@Controller('pricing')
export class PricingController {
@Get()
find(@Geo() visitor: GeoLookup) {
const currency =
visitor.status === 'resolved' ? visitor.value.country?.currency ?? 'USD' : 'USD';
return this.catalog.pricedIn(currency);
}
}Next.js
createGeo() works in Route Handlers, Server Components and edge middleware.ts - pass it the request headers and it selects the caller IP for you.
import { headers } from 'next/headers';
import { createGeo } from '@ipgeotrace/next';
const geo = createGeo({ apiKey: process.env.IPGEOTRACE_API_KEY! });
export async function GET() {
const visitor = await geo.resolve(await headers());
const currency =
visitor.status === 'resolved' ? visitor.value.country?.currency ?? 'USD' : 'USD';
return Response.json({ currency });
}Configuration#
Every option is optional. Point at the sandbox while developing, turn on caching once you see repeat traffic, and tune timeout and retries to taste.
const geo = new IpGeoTraceClient({
apiKey: process.env.IPGEOTRACE_API_KEY!,
environment: 'sandbox', // production | sandbox
cache: true, // in-memory cache, or pass your own GeoCache (Redis, ...)
cacheTtlMs: 6 * 60 * 60 * 1000,
timeoutMs: 3000,
maxRetries: 0,
});Caller IP & skipping
By default each adapter trusts the framework's own IP (enable trust proxy behind a load balancer). Override selection with ipSelector, and skip health checks or internal routes with shouldResolve.
app.use(
ipgeotrace({
apiKey: process.env.IPGEOTRACE_API_KEY!,
shouldResolve: (req) => !req.path.startsWith('/internal'),
ipSelector: (req) => req.headers['cf-connecting-ip'] as string,
}),
);Caching, timeout & retries
Caching is off by default; turn it on and repeat lookups of the same IP are served from memory - no API call, no quota. Only successful lookups are cached. The per-request timeout defaults to 10 seconds, and rate limits (429) and outages (503) are retried automatically, honoring the Retry-After header - two retries by default. Pass your own GeoCache to back the cache with Redis or a distributed store.
Building for the browser?
The core never runs client-side. To personalize a page from the visitor's location, use @ipgeotrace/browser with a publishable key instead.