summaryrefslogtreecommitdiffstats
path: root/README.md
diff options
context:
space:
mode:
authorMurilo Ijanc <[email protected]>2026-07-26 12:13:27 -0300
committerMurilo Ijanc <[email protected]>2026-07-26 12:13:27 -0300
commit625cab052606cb3c2a7c4e008c43cf536b6404ce (patch)
tree80d126ce42750d29eec42d9fef98a68341bb2dfa /README.md
downloadgo-625cab052606cb3c2a7c4e008c43cf536b6404ce.tar.gz
Initial import
Diffstat (limited to 'README.md')
-rw-r--r--README.md219
1 files changed, 219 insertions, 0 deletions
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..63f0e96
--- /dev/null
+++ b/README.md
@@ -0,0 +1,219 @@
+# cepis
+
+Go client for the [public CEP API at cep.is](https://cep.is). Official Correios
+data (DNE), refreshed every two weeks. No dependencies beyond the standard
+library.
+
+```go
+addr, err := cepis.Lookup(ctx, "01001-000")
+fmt.Println(addr.Logradouro, addr.Localidade, addr.UF)
+// Praça da Sé São Paulo SP
+```
+
+## Installation
+
+```sh
+go get cep.is/go
+```
+
+Requires Go 1.21 or newer.
+
+```go
+import cepis "cep.is/go"
+```
+
+> `cep.is/go` is a vanity import path and needs a `go-import` meta tag served by
+> cep.is; that route does not exist yet, so `go get` will not resolve until it
+> does.
+
+## Usage
+
+### A single lookup
+
+`Lookup` accepts the CEP masked or unmasked and returns an `*Address`:
+
+```go
+a, err := cepis.Lookup(ctx, "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. `a.Field("name")` reads any field of the raw response, including
+one the API grew after this release.
+
+### Coordinates
+
+About 57% of CEPs have a coordinate. When they do not, `Geo` is nil:
+
+```go
+if a.Geo != nil {
+ fmt.Println(a.Geo.Lat, a.Geo.Lon) // -23.567202 -46.649659
+ fmt.Println(a.Geo.RaioM) // 1084
+ fmt.Println(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"`:
+
+```go
+if a.Geo != nil && a.Geo.IsPrecise() {
+ cost = shippingFrom(a.Geo.Lat, a.Geo.Lon)
+} else {
+ cost = shippingFromMunicipality(a.IBGE)
+}
+```
+
+### Many CEPs at once
+
+Do not loop over `Lookup`. Use `LookupMany`, which resolves up to 500 CEPs per
+request:
+
+```go
+ceps := []string{"01001000", "99999999", "20040020"}
+result, err := cepis.LookupMany(ctx, ceps)
+
+for i, addr := range result {
+ if addr == nil {
+ fmt.Printf("%s: not found\n", ceps[i])
+ } else {
+ fmt.Printf("%s: %s\n", ceps[i], addr.Localidade)
+ }
+}
+```
+
+The result has **the same length and the same order** as the input. A CEP that
+does not exist, or is malformed, is nil at its position — it does not vanish,
+and it is not an error.
+
+Slices 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 a nil `Geo`,
+> 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:
+
+```go
+h, err := cepis.CheckHealth(ctx)
+
+h.OK() // true
+h.DNE // "26071" identifier of the fortnightly database
+h.CEPs // 1596168
+h.BaseGeradaEm // "2026-07-24T03:48:28Z"
+```
+
+An API reporting itself degraded is not an error — that arrives in `h.Status`.
+
+## Configuration
+
+For anything beyond the defaults, build a `Client` and reuse it. It keeps no
+state between calls and is safe to share across goroutines:
+
+```go
+client, err := cepis.New(
+ cepis.WithBaseURL("https://cep.is"), // point at another instance
+ cepis.WithTimeout(10*time.Second), // cap on one attempt
+ cepis.WithMaxAttempts(3), // counting the first; 1 disables retrying
+ cepis.WithBackoff(500*time.Millisecond, 8*time.Second),
+ cepis.WithTotalTimeout(15*time.Second), // cap on the whole call
+ cepis.WithUserAgent("myapp/1.0"),
+ cepis.WithHTTPClient(&http.Client{}),
+)
+
+addr, err := client.Lookup(ctx, "01001000")
+```
+
+`WithTimeout` bounds a single attempt — connection, response and body read
+together. A call may make several attempts with waits between them; the cap on
+all of that is `WithTotalTimeout`. A deadline on the context passed to a call is
+honoured too, and the earlier of the two wins.
+
+Retries cover 429, 5xx and network failures, with exponential backoff and full
+jitter; 400 and 404 are never retried. See the [package
+documentation](https://pkg.go.dev/cep.is/go) for each option.
+
+The package-level functions (`cepis.Lookup`, etc.) use a shared default client,
+reachable as `cepis.Default()`. It applies process-wide, third-party libraries
+included. Need a different configuration? Build your own.
+
+### Pointing at a local instance
+
+`WithBaseURL` is the only setting that usually changes between environments. The
+API repo brings a local instance up with `just up`:
+
+```go
+client, err := cepis.New(cepis.WithBaseURL("http://localhost:8080"))
+```
+
+## Errors
+
+Every error matches `cepis.Err`. 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? |
+| --- | --- | --- |
+| `ErrInvalidCEP` | not 8 digits | no |
+| `ErrCEPNotFound` | well-formed, not in the database | no |
+| `ErrRateLimited` | 429 | yes, later |
+| `ErrServer` | 5xx | yes, later |
+| `ErrNetwork` | DNS, connection, timeout, TLS | yes, later |
+| `ErrInvalidResponse` | unintelligible response | probably not |
+| `ErrInvalidConfig` | a bad option given to `New` | no |
+
+```go
+addr, err := cepis.Lookup(ctx, cepFromForm)
+switch {
+case errors.Is(err, cepis.ErrCEPNotFound):
+ // ask the user for another CEP
+case errors.Is(err, cepis.Err):
+ // transient failure: reschedule
+}
+```
+
+The concrete type behind every one of them is `*cepis.Error`, which carries the
+CEP, the HTTP status and the server's `Retry-After`:
+
+```go
+var cerr *cepis.Error
+if errors.As(err, &cerr) && errors.Is(cerr, cepis.ErrRateLimited) {
+ reschedule(cerr.RetryAfter)
+}
+```
+
+`ErrInvalidResponse` 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`:
+
+```sh
+make # list the targets
+make test # run the suite
+make test-race # run it under the race detector
+make lint # gofmt -l + go vet, touches nothing
+make build # compile every package
+make check # lint, then test, then build
+```
+
+## Licence
+
+ISC. See [LICENSE](LICENSE).