summaryrefslogtreecommitdiffstats
path: root/README.md
diff options
context:
space:
mode:
authorMurilo Ijanc <[email protected]>2026-07-26 12:15:55 -0300
committerMurilo Ijanc <[email protected]>2026-07-26 12:15:55 -0300
commit986a16c34049187a6c1f39f38bfa59e327093494 (patch)
tree0c0bab4521fa3e8a4245b534505f56816a6f250b /README.md
downloadtypescript-986a16c34049187a6c1f39f38bfa59e327093494.tar.gz
Initial importv0.1.0
Diffstat (limited to 'README.md')
-rw-r--r--README.md214
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).