diff options
| author | Murilo Ijanc <[email protected]> | 2026-07-26 12:15:55 -0300 |
|---|---|---|
| committer | Murilo Ijanc <[email protected]> | 2026-07-26 12:15:55 -0300 |
| commit | 986a16c34049187a6c1f39f38bfa59e327093494 (patch) | |
| tree | 0c0bab4521fa3e8a4245b534505f56816a6f250b /README.md | |
| download | typescript-986a16c34049187a6c1f39f38bfa59e327093494.tar.gz | |
Initial importv0.1.0
Diffstat (limited to 'README.md')
| -rw-r--r-- | README.md | 214 |
1 files changed, 214 insertions, 0 deletions
diff --git a/README.md b/README.md new file mode 100644 index 0000000..009f7df --- /dev/null +++ b/README.md @@ -0,0 +1,214 @@ +# cepis + +TypeScript client for the [public CEP API at cep.is](https://cep.is). Official +Correios data (DNE), refreshed every two weeks. Works from JavaScript too. + +```ts +import { lookup } from "cepis"; + +const address = await lookup("01001-000"); +console.log(address.logradouro, address.localidade, address.uf); +// Praça da Sé São Paulo SP +``` + +## Installation + +```sh +bun add cepis +# or: npm install cepis +``` + +Zero runtime dependencies. Requires a runtime with `fetch` and +`AbortSignal.timeout`: Node 18+, Bun, Deno, or a browser. Ships ESM and type +declarations. + +## Usage + +### A single lookup + +`lookup` accepts the CEP masked or unmasked and resolves to an `Address`: + +```ts +import { lookup } from "cepis"; + +const a = await lookup("01310-100"); + +a.logradouro; // "Avenida Paulista" +a.localidade; // "São Paulo" +a.uf; // "SP" +a.ibge; // "3550308" +a.cep; // "01310100" (always unmasked) +a.formattedCep; // "01310-100" +``` + +Fields the database does not have for that CEP come back as empty strings, +never `null`. The data fields keep the API's own Portuguese names; everything +else — methods, types, errors — is English. + +### Coordinates + +About 57% of CEPs have a coordinate. When they do not, `geo` is `null`: + +```ts +const a = await lookup("01310100"); + +if (a.geo) { + a.geo.lat; // -23.567202 + a.geo.lon; // -46.649659 + a.geo.raio_m; // 1084 + a.geo.precisao; // "logradouro" +} +``` + +**Check `precisao` before computing distance.** There are two kinds of point, +and the difference between them is tens of kilometres: + +| `precisao` | typical radius | what the point is | +| ------------ | -------------- | ------------------------------------------------------ | +| `logradouro` | 110–140 m | the stretch of street — good for maps and distance | +| `localidade` | 7–40 km | the municipality centroid: the CEP _is_ the whole city | + +`a.geo.isPrecise` is the shorthand for `precisao === "logradouro"`: + +```ts +const cost = + a.geo && a.geo.isPrecise ? shippingFrom(a.geo.lat, a.geo.lon) : shippingFromMunicipality(a.ibge); +``` + +### Many CEPs at once + +Do not loop over `lookup`. Use `lookupMany`, which resolves up to 500 CEPs per +request: + +```ts +import { lookupMany } from "cepis"; + +const ceps = ["01001000", "99999999", "20040020"]; +const result = await lookupMany(ceps); + +ceps.forEach((requested, i) => { + const address = result[i]; + console.log(requested, address === null ? "not found" : address.localidade); +}); +``` + +The returned array has **the same length and the same order** as the input. A +CEP that does not exist, or is malformed, becomes `null` at its position — it +does not vanish, and it does not throw. + +Arrays longer than 500 are chunked automatically. + +> **Batches carry no coordinate.** The endpoint behind them is CSV, and the CSV +> has no `geo` column. Every `Address` from `lookupMany` has `geo === null`, +> including for CEPs that do have one. Need `geo`? Use `lookup`, one at a time. + +### API health + +Useful for two things: checking the API answers, and knowing **which version of +the database** is live: + +```ts +import { health } from "cepis"; + +const h = await health(); +h.ok; // true +h.dne; // "26071" identifier of the fortnightly database +h.ceps; // 1596168 +h.base_gerada_em; // "2026-07-24T03:48:28Z" +``` + +An API reporting itself degraded does not throw — that arrives in `h.status`. + +## Configuration + +For anything beyond the defaults, build a `Client` and reuse it. It keeps no +state between calls: + +```ts +import { Client } from "cepis"; + +const client = new Client({ + baseUrl: "https://cep.is", // point at another instance + timeoutMs: 10_000, // per attempt, request and body read included + maxAttempts: 3, // counting the first; 1 disables retrying + backoffBaseMs: 500, // base of the exponential wait + backoffMaxMs: 8_000, // cap on each wait + totalTimeoutMs: undefined, // cap on the whole call, attempts included +}); + +const address = await client.lookup("01001000"); +``` + +Retries cover 429, 5xx and network failures, with exponential backoff and +jitter; 400 and 404 are never retried. + +The module-level functions (`lookup`, `lookupMany`, `health`) use a shared +default client. Need a different configuration? Build your own. + +### Pointing at a local instance + +`baseUrl` is the only option that usually changes between environments. The API +repo brings a local instance up with `just up`: + +```ts +const client = new Client({ baseUrl: "http://localhost:8080" }); +``` + +From the command line, to confirm quickly that it answers: + +```sh +bunx cepis --base-url http://localhost:8080 01001000 +``` + +## Errors + +Everything derives from `CepisError`. Below that, the split is by **the decision +you have to make**, not by the HTTP status that caused it: + +| error | when | does retrying help? | +| ----------------- | -------------------------------- | ------------------- | +| `InvalidCEP` | not 8 digits | no | +| `CEPNotFound` | well-formed, not in the database | no | +| `RateLimited` | 429 | yes, later | +| `ServerError` | 5xx | yes, later | +| `NetworkError` | DNS, connection, timeout, TLS | yes, later | +| `InvalidResponse` | unintelligible response | probably not | + +```ts +import { CEPNotFound, CepisError, lookup } from "cepis"; + +try { + const address = await lookup(cepFromForm); +} catch (error) { + if (error instanceof CEPNotFound) { + // ask the user for another CEP + } else if (error instanceof CepisError) { + // transient failure: reschedule + } else { + throw error; + } +} +``` + +`RateLimited` exposes `retryAfterMs` and `ServerError` exposes `status`. + +`InvalidResponse` almost always means something between you and the API is +answering instead of it — a captive Wi-Fi portal, a corporate proxy, an HTML +error page. Useful as a diagnosis: the problem is not in your code. + +## Development + +Everything runs through `make`, which shells out to `bun`: + +```sh +make # list the targets +make test # bun test +make lint # tsc --noEmit + prettier --check, touches nothing +make format # apply prettier +make build # bundle to dist/ and emit .d.ts +make check # lint, then test, then build +``` + +## Licence + +ISC. See [LICENSE](LICENSE). |