@ipgeotrace/nestjs
v0.1.0
Published
NestJS module for IPGeoTrace. Resolves the caller's location once per request and injects it via the @Geo() decorator.
Maintainers
Readme
@ipgeotrace/nestjs
NestJS module for IPGeoTrace. Resolves the caller's location once per
request and injects it into your handlers with the @Geo() decorator. Built on
@ipgeotrace/client — the secret key stays server-side.
Sign up and grab your API key at ipgeotrace.com.
Install
npm add @ipgeotrace/nestjs @nestjs/common @nestjs/core rxjs reflect-metadataUsage
Register the module once at the root. It binds a global interceptor that resolves the caller on every request, so nothing else needs wiring.
import { Module } from '@nestjs/common';
import { IpGeoTraceModule } from '@ipgeotrace/nestjs';
@Module({
imports: [
IpGeoTraceModule.forRoot({ apiKey: process.env.IPGEOTRACE_API_KEY! }),
],
})
export class AppModule {}Then pull the result into any handler with @Geo():
import { Controller, Get } from '@nestjs/common';
import { Geo, type GeoLookup } from '@ipgeotrace/nestjs';
@Controller('checkout')
export class CheckoutController {
@Get()
checkout(@Geo() geo: GeoLookup) {
const currency = geo.status === 'resolved' ? geo.value?.country?.currency ?? 'USD' : 'USD';
return { currency };
}
}@Geo() gives you a GeoLookup whose status tells you exactly what happened:
resolved—valuecarries the data.skipped— the caller's IP was missing, private, loopback, or link-local, so no API call was made.failed—errorcarries the reason (rate_limited,quota_exceeded, …).not_attempted— the interceptor opted out for this request (shouldResolvereturned false), or never ran (non-HTTP context).
Skipping requests
Health checks and internal endpoints should not spend lookups. Opt them out with shouldResolve:
IpGeoTraceModule.forRoot({
apiKey: process.env.IPGEOTRACE_API_KEY!,
shouldResolve: (req) => !req.url.startsWith('/health'),
});Private, loopback, and link-local addresses (including IPv4-mapped IPv6) are detected locally and skipped without an API call or quota usage.
Choosing the caller's IP
By default the interceptor uses req.ip, which respects your HTTP adapter's proxy settings (enable
trust proxy on Express or trustProxy on Fastify). Override it for full control:
IpGeoTraceModule.forRoot({
apiKey: process.env.IPGEOTRACE_API_KEY!,
ipSelector: (req) => req.headers['cf-connecting-ip'] ?? req.ip,
});Async configuration
Pull the API key (and any client options) from ConfigService or another provider:
IpGeoTraceModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
apiKey: config.getOrThrow('IPGEOTRACE_API_KEY'),
clientOptions: { cache: true, timeoutMs: 3_000 },
}),
});Injecting the service directly
IpGeoTraceService is exported from the module and injectable anywhere for explicit lookups (e.g.
resolving an IP you already have, or in a queue worker):
import { Injectable } from '@nestjs/common';
import { IpGeoTraceService } from '@ipgeotrace/nestjs';
@Injectable()
export class FraudService {
constructor(private readonly geo: IpGeoTraceService) {}
async score(ip: string) {
const result = await this.geo.resolve(ip);
return result.ok ? result.value.country?.code : undefined;
}
}Opting out of the global interceptor
Pass useGlobalInterceptor: false and apply GeoInterceptor selectively instead:
import { UseInterceptors } from '@nestjs/common';
import { GeoInterceptor } from '@ipgeotrace/nestjs';
@UseInterceptors(GeoInterceptor)
@Controller('checkout')
export class CheckoutController {}See the @ipgeotrace/client README for caching, retries, timeouts, and batch lookups.
