@netcov-io/client
v0.1.0
Published
TypeScript client for the NetCov API (netcov.io) — internet/broadband coverage data (Italy coverage + address geocoding, ANNCSU / Istat – Agenzia delle Entrate data)
Maintainers
Readme
@netcov-io/client
A typed, zero-dependency TypeScript client for the NetCov API (netcov.io) — internet/broadband coverage data (Italy coverage + address geocoding)
(comuni, addresses, street numbers and fixed-broadband coverage for Italy).
Works on Node.js 18+ and in every modern browser (uses the global
fetch).
Fonte: Istat – Agenzia delle Entrate, Archivio Nazionale dei Numeri Civici e delle Strade Urbane (ANNCSU)
Attribution is mandatory (CC BY 4.0 data): surface the attribution
string wherever you display, redistribute or export geocoded data. It is
included on every response as the attribution field and exported as
ATTRIBUTION from this package.
Install
npm i @netcov-io/clientAI coding agents
If you are an AI coding agent (or working with one), read
SKILL.md and run the interactive onboarding:
npx @netcov-io/client initIt resolves NETCOV_API_KEY (flag, env, or .env), validates it with
GET {baseUrl}/credits, and prints a copy-paste first call. No key yet?
Sign up at https://netcov.io/portal (about a minute), then re-run init.
LLM reference: https://netcov.io/llms.txt.
Quickstart
import { NetCovClient } from '@netcov-io/client';
const client = new NetCovClient({ apiKey: 'your-api-key' });
const { results, headers } = await client.geocode('Milano, Corso Buenos Aires 1', {
limit: 5,
});
console.log(results[0].latitude, results[0].longitude);
console.log(headers.remaining); // tokens left in the burst bucketConfiguration
new NetCovClient({
baseUrl: 'http://localhost:8788', // default
apiKey: 'your-api-key',
// fetch: myFetch, // optional custom fetch implementation
// signal: abortSignal, // optional default abort signal
// retryServerErrors: true, // opt-in 502/503/504 retry (default false)
});Values can also come from environment variables:
| Variable | Default |
| --- | --- |
| NETCOV_BASE_URL | http://localhost:8788 (dev server) |
| NETCOV_API_KEY | (none — no Authorization header is sent) |
Constructor options take precedence over environment variables.
Browser usage & key security
API keys must not ship in public JS bundles — anything in client-side code is extractable. Keep secret keys server-side (or in a backend proxy) and never commit them.
Browser use requires an origin-locked key (restricted to your site's origin). Use a key with an origin allowlist for frontend deployments so a leaked key cannot be reused elsewhere.
API
Every method resolves with the typed JSON payload of the endpoint, with the
parsed X-RateLimit-* headers attached as a non-enumerable headers
property. The property does not affect JSON.stringify() or Object.keys(),
but is directly accessible (and destructurable).
const data = await client.geocode('Roma');
data.headers; // { limit, remaining, reset, dailyRemaining, monthlyRemaining, retryAfter }
JSON.stringify(data); // unchanged API payloadgeocode(address, opts?)
Geocode a free-form Italian address.
const { results } = await client.geocode('Milano, Corso Buenos Aires 1');
// opts: { limit?: number /* 1-20 */, strict?: boolean, signal?: AbortSignal }GeocodeResult fields: query, match, latitude, longitude, comune,
provincia, regione, cap, civico, confidence
(high | comune | low), source, score.
The response carries a top-level notice: string | null. It is null for
civic-level (high) matches and set (e.g.
"no civic-level match; fell back to the comune centroid (Bari)") when the
address fell back to the comune centroid. Pass strict: true to reject
non-civic matches and no-match queries with a 404 address_not_found error
instead of the lenient fallback.
coverageItaly(params, opts?)
Fixed-broadband coverage for a point in Italy, by address or lat+lon
(or both).
const { cells } = await client.coverageItaly({ lat: 43.9760736, lon: 10.944691 });
const byAddress = await client.coverageItaly({ address: 'Via Crespole e Fabbriche 125, Pistoia' });
// opts: { limit?: number, strict?: boolean, signal?: AbortSignal }CoverageCell fields: technologies (technology ∈ adsl | vdsl |
vdsl-vectoring | ftth | ftb | fttc | fttn | docsis | docsis-3.0 |
docsis-3.1 | 2g | 3g | lte | 4g | 5g | 5g-fwa | fwa | satellite |
other), best_speed_mbps, best_speed_tier (no-coverage | basic | fast |
ultrafast | gigabit), operator (string | null).
For address queries the same notice / strict semantics as geocode
apply (notice is always null for coordinate queries).
autocomplete({ city?, address?, limit? }, opts?)
Street suggestions when address is given (filtered by city); comune
suggestions when address is empty.
const streets = await client.autocomplete({ city: 'Firenze', address: 'via del tirat' });
const comuni = await client.autocomplete({ city: 'To', limit: 10 });
// opts: { signal?: AbortSignal }Street results use kind: 'street' with a street field; comune results use
kind: 'comune' with a name field (plus regione).
groupTechnologies(coverage)
Group a coverage response per technology for UI rendering — one row per
technology with the best declared downlink, its tier and the deduplicated
operator list, sorted by max downlink descending (nulls last). Same
grouping semantics as the coverage summary (operators are
semicolon-split, trimmed and deduplicated).
import { groupTechnologies } from '@netcov-io/client';
const coverage = await client.coverageItaly({ lat: 43.9760736, lon: 10.944691 });
for (const g of groupTechnologies(coverage)) {
console.log(g.technology, g.maxDownlinkMbps, g.bestSpeedTier, g.operators);
}
// [{ technology: 'ftth', maxDownlinkMbps: 1000, bestSpeedTier: 'gigabit', operators: ['Open Fiber'] }, …]Errors & rate limits
Requests are governed by a token bucket that refills at rpm/60 tokens per
second and holds at most ceil(rpm/12) tokens (~5 seconds of headroom). You
can fire up to the burst cap instantly; afterwards you get roughly one
request per refill interval (e.g. rpm 30 → burst of 3, then 1 every 2 s). The
long-run rate is still rpm per minute. The 4th instant call over the burst
cap returns HTTP 429 rate_limit_exceeded with a Retry-After header.
All failures throw an ApiError subclass — catch them all with
err instanceof ApiError:
| Error class | When | Fields of interest |
| --- | --- | --- |
| NetworkError | request never reached the server (status: 0) | cause |
| AuthenticationError | HTTP 401 — missing/invalid/revoked token | status |
| RateLimitError | HTTP 429 — rate/quota cap exceeded | retryAfter, headers |
| ServerError | HTTP 5xx — service/DB unavailable | status |
| ApiError | any other 4xx/5xx, or non-JSON body | status, code, body |
Every error carries status, code, headers (RateLimitInfo | null) and,
on 429 responses, retryAfter (seconds, parsed from the Retry-After
header — both integer seconds and HTTP-date formats are supported).
requestId / docUrl come straight from the error envelope — quote
requestId in support tickets.
See the API error catalog for every code: https://netcov.io/errors.
Handling 429 / Retry-After
import { RateLimitError } from '@netcov-io/client';
try {
const data = await client.geocode('Milano');
} catch (err) {
if (err instanceof RateLimitError) {
const waitMs = (err.retryAfter ?? 60) * 1000;
setTimeout(() => retry(), waitMs); // respect Retry-After, don't hammer
} else {
throw err;
}
}Retrying 502/503/504 (opt-in)
By default server errors throw immediately. Pass retryServerErrors: true
to retry idempotent GETs on 502/503/504 up to 3 extra times with
backoff 0.5s, 1s, 2s. A retry_after envelope field or Retry-After
header on the failed response overrides the scheduled delay for that
attempt. HTTP 429 responses are never retried automatically.
const client = new NetCovClient({ apiKey: 'your-api-key', retryServerErrors: true });Response headers
Every response carries X-RateLimit-Limit (the rpm), X-RateLimit-Remaining
(tokens left in the burst bucket), X-RateLimit-Reset (seconds until the
next token refills), X-RateLimit-Daily-Remaining and
X-RateLimit-Monthly-Remaining. They are exposed in parsed form:
interface RateLimitInfo {
limit: number | null; // rpm
remaining: number | null; // burst-bucket tokens left (cap ceil(rpm/12))
reset: number | null; // seconds until the next token refills
dailyRemaining: number | null;
monthlyRemaining: number | null;
retryAfter: number | null; // seconds
}Design notes
- Zero runtime dependencies. Only
fetch(global) is used. - Response shape. Methods return the API payload directly, with
headersattached non-enumerably — your data stays clean and serializable. - Enums.
Technology,SpeedTierandConfidenceare string enums, so runtime checks (result.confidence === Confidence.High) and string comparisons both work. - Dual module support. ESM (
import) and CommonJS (require) builds are shipped underdist/esmanddist/cjs.
Development
npm install
npm run build # tsc (ESM + CJS) + dist marker files
npm test # vitest: mocked-fetch unit tests + live integration (see below)Integration tests hit the local dev API at http://127.0.0.1:8788 and are
skipped unless NETCOV_API_KEY is set. The CI key lives in the
gitignored .env.test:
node --env-file=.env.test node_modules/.bin/vitest runNever commit the API key.
License
MIT — code. Geocoding data is © Istat – Agenzia delle Entrate, Archivio Nazionale dei Numeri Civici e delle Strade Urbane (ANNCSU), CC BY 4.0.
