Skip to content
This repository was archived by the owner on Oct 12, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 99 additions & 67 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,71 +4,103 @@ Data Validator API

> Upwork portfolio thumbnail — Data validation API (email, CPF, phone, etc.)

The Data Validator API is a service designed to validate common data inputs, such as email, CPF, name, phone number, RG, CEP, and credit card number. It supports flexible input formats, sanitizes data to ensure consistency, and returns structured JSON responses.

Features

- Email Validation: Verifies email format and normalizes case.
- CPF Validation: Checks Brazilian CPF validity (including check digits) and caches result in Redis.
- Name Validation: Removes accents, strips invalid chars, normalizes spaces, and converts to uppercase.
- Phone Validation: Validates Brazilian phone numbers with DDD and mobile rules.
- RG Validation: Validates basic Brazilian RG format.
- CEP Validation: Validates CEP length and basic invalid values.
- Credit Card Validation: Uses Luhn algorithm and identifies card brand.

API Endpoint

GET /validate

This endpoint receives query parameters and validates exactly one parameter at a time.

Query Parameters

- email (optional) - Email address to validate.
- cpf (optional) - Brazilian CPF number to validate, accepts different formats.
- name (optional) - Name to validate and sanitize.
- telephone (optional) - Phone number to validate.
- phone (optional) - Alias for `telephone`.
- plastic (optional) - Credit card number to validate.
- rg (optional) - Brazilian RG to validate.
- cep (optional) - CEP (postal code) to validate.

Status Codes

- 200: Valid value.
- 400: Missing parameter or multiple parameters sent.
- 422: Parameter provided but failed validation.

Response Format

The API returns a JSON object with the validation result:

- status_code: HTTP status code.
- error_code: Optional machine-readable error code.
- parameter_key: Input parameter key.
- raw_parameter_value: Original input.
- parameter_value: Sanitized value.
- is_valid: Whether the value is valid.
- message: Human-readable message.
- request_id: Unique request identifier.
- timestamp: Request timestamp.
- execution_time_ms: Processing time.
- from_cache: Whether result came from cache (CPF only).

Examples

- Valid email
- Request: `GET /validate?email=USER@Test.COM`
- Response: `200` with `parameter_value: "user@test.com"` and `is_valid: true`.

- Invalid CPF
- Request: `GET /validate?cpf=111.111.111-11`
- Response: `422` with `error_code: "VALIDATION_FAILED"` and `is_valid: false`.

- Valid phone using alias
- Request: `GET /validate?phone=+55 (11) 91234-5678`
- Response: `200` with `parameter_key: "telephone"` and `parameter_value: "11912345678"`.

- Multiple parameters (not allowed)
- Request: `GET /validate?email=a@a.com&cpf=52998224725`
- Response: `400` with `error_code: "MULTIPLE_PARAMETERS"`.
A Go library for validating Brazilian documents, and an HTTP service built on it.

```bash
go get github.com/jeferson0306/api-data-validator/validate
```

```go
result := validate.CPF("529.982.247-25")
result.Valid // true
result.Normalized // "52998224725"
```

The library is pure: no network, no cache, no configuration, no state. Importing
it opens no connections and reads no environment. Validation is arithmetic, and
arithmetic that needs a server is arithmetic you cannot trust when the server is
down.

Values are **checked, not laundered**. Formatting a person legitimately types —
dots, dashes, spaces, the slash in a CNPJ — is accepted and removed. Anything
else is a rejection. Stripping unexpected characters and validating what remains
accepts `abc529.982.247-25` as a CPF, which is how a validator ends up letting
junk into a database.

## What it checks

| | |
|---|---|
| People | CPF, PIS/PASEP/NIT/NIS, RG, título de eleitor, CNH, full name |
| Companies | CNPJ, inscrição estadual (**all 27 states**) |
| Either | `documento` — whichever of CPF or CNPJ the digits describe |
| Vehicles | RENAVAM, plate (pre-Mercosul and Mercosul) |
| Money | PIX key (all five forms), card number with brand, boleto linha digitável |
| Contact | email, phone, CEP |

Every algorithm was checked against a published reference or the issuing state's
own worked example before it shipped. Two of them disagree with the reference
implementation they were compared against, in both cases because the reference
is wrong — see `validate/inscricao_estadual_test.go`.

## Command line

```bash
go install github.com/jeferson0306/api-data-validator/cmd/brdoc@latest

brdoc cpf 529.982.247-25 && echo accepted
brdoc ie 0100482300112 AC
brdoc --json cnpj 33.000.167/0001-01 | jq .normalized
brdoc list
```

Exit status is 0 for valid, 1 for invalid and 2 for a usage error, so it
composes with everything else a shell can do.

## HTTP service

```
GET /validate?cpf=529.982.247-25
GET /validate?ie=0100482300112&uf=AC
POST /validate/batch one form, one request
GET /health
GET /swagger/index.html
```

`POST /validate/batch` takes a list of items, each with its own type, and
answers one result per item plus a summary. A form with eight fields is one
request rather than eight.

## Performance

```
BenchmarkCPF-12 522 ns/op
BenchmarkCNPJ-12 499 ns/op
BenchmarkBoleto-12 864 ns/op
BenchmarkStateRegistration-12 248 ns/op
```

Worth knowing before reaching for a cache: a CPF check costs about half a
microsecond, against a Redis round trip measured at 150–220 ms in production.
The cache is opt-in through `REDIS_ADDR` and off by default.

## Layout

```
validate/ the library — pure, importable, no dependencies beyond x/text
internal/cache/ the service's optional CPF cache, kept out of the library
handlers/ HTTP handlers
middleware/ CORS
observability/ request logging that never records the value being validated
cmd/brdoc/ the command line tool
main.go the service
```

## Development

```bash
go test ./... -race
go test ./validate -bench .
go run ./cmd/brdoc cpf 529.982.247-25
```
97 changes: 97 additions & 0 deletions cmd/brdoc/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Command brdoc checks Brazilian documents from the shell.
//
// It exits 0 when the value is valid and 1 when it is not, so it composes with
// everything else a shell can do:
//
// brdoc cpf 529.982.247-25 && echo accepted
// brdoc ie 0100482300112 AC
// brdoc --json cnpj 33.000.167/0001-01 | jq .normalized
//
// Nothing leaves the machine. The library it wraps performs arithmetic and
// makes no network calls, which is the point of having a CLI at all.
package main

import (
"encoding/json"
"flag"
"fmt"
"os"
"strings"

"github.com/jeferson0306/api-data-validator/validate"
)

const (
exitValid = 0
exitInvalid = 1
exitUsage = 2
)

func main() {
asJSON := flag.Bool("json", false, "print the result as JSON")
flag.Usage = usage
flag.Parse()

args := flag.Args()
if len(args) == 1 && args[0] == "list" {
fmt.Println(strings.Join(validate.Keys(), "\n"))
os.Exit(exitValid)
}

if len(args) < 2 {
usage()
os.Exit(exitUsage)
}

key, value := args[0], args[1]
qualifier := ""
if len(args) > 2 {
qualifier = args[2]
}

result, known := validate.ByKey(key, value, qualifier)
if !known {
fmt.Fprintf(os.Stderr, "unknown document type %q\nknown types: %s\n",
key, strings.Join(validate.Keys(), ", "))
os.Exit(exitUsage)
}

if *asJSON {
// Field names match the library's, so a script reading this and a
// program importing the package see the same shape.
_ = json.NewEncoder(os.Stdout).Encode(map[string]any{
"valid": result.Valid,
"normalized": result.Normalized,
"reason": result.Reason,
})
} else {
mark := "invalid"
if result.Valid {
mark = "valid"
}
fmt.Printf("%-8s %s %s\n", mark, result.Normalized, result.Reason)
}

if !result.Valid {
os.Exit(exitInvalid)
}
}

func usage() {
fmt.Fprint(os.Stderr, `brdoc — check Brazilian documents

usage:
brdoc [--json] <type> <value> [qualifier]
brdoc list

The qualifier is only used by "ie", which takes the issuing state:
brdoc ie 0100482300112 AC

exit status:
0 valid
1 invalid
2 usage error

`)
flag.PrintDefaults()
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module DataValidatorAPI
module github.com/jeferson0306/api-data-validator

go 1.22.5

Expand Down
25 changes: 18 additions & 7 deletions handlers/batch.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import (
"strings"
"time"

"DataValidatorAPI/models"
"DataValidatorAPI/utils"
"github.com/jeferson0306/api-data-validator/internal/cache"
"github.com/jeferson0306/api-data-validator/models"
"github.com/jeferson0306/api-data-validator/validate"

"github.com/google/uuid"
)
Expand Down Expand Up @@ -101,21 +102,31 @@ func validateItem(item models.BatchItem) models.BatchResult {
return result
}

outcome, known := utils.Validate(key, item.Value, item.Qualifier)
// CPF is the one check routed through the cache, so that from_cache means
// the same thing on both endpoints.
if key == "cpf" {
outcome, fromCache := cache.CPF(item.Value)
result.Value = outcome.Normalized
result.IsValid = outcome.Valid
result.Message = outcome.Reason
result.FromCache = fromCache
return result
}

outcome, known := validate.ByKey(key, item.Value, item.Qualifier)
if !known {
// Distinguished from a failed validation on purpose: an unknown key is
// the caller's mistake, and reporting it as "invalid" would suggest the
// value was checked and rejected.
result.ErrorCode = "UNKNOWN_KEY"
result.Message = "Unknown document type " + key +
" (expected one of " + strings.Join(utils.SupportedKeys(), ", ") + ")"
" (expected one of " + strings.Join(validate.Keys(), ", ") + ")"
return result
}

result.Value = outcome.Sanitized
result.Value = outcome.Normalized
result.IsValid = outcome.Valid
result.Message = outcome.Message
result.FromCache = outcome.FromCache
result.Message = outcome.Reason
return result
}

Expand Down
2 changes: 1 addition & 1 deletion handlers/batch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"strings"
"testing"

"DataValidatorAPI/models"
"github.com/jeferson0306/api-data-validator/models"
)

func postBatch(t *testing.T, body string) (*httptest.ResponseRecorder, models.BatchResponse) {
Expand Down
16 changes: 11 additions & 5 deletions handlers/health.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package handlers
import (
"net/http"

"DataValidatorAPI/utils"
"github.com/jeferson0306/api-data-validator/internal/cache"

"github.com/gin-gonic/gin"
)
Expand All @@ -17,13 +17,19 @@ import (
// visible — a silently unreachable Redis is how this service spent its first
// deployment paying latency for a cache that never returned a hit.
func HealthHandler(c *gin.Context) {
cache := "unreachable"
if utils.CacheHealthy() {
cache = "ok"
// A local named "cache" would shadow the package; more importantly, there
// are now three states rather than two. "disabled" is not a fault.
status := "disabled"
switch {
case !cache.Enabled():
case cache.Healthy():
status = "ok"
default:
status = "unreachable"
}

c.JSON(http.StatusOK, gin.H{
"status": "ok",
"cache": cache,
"cache": status,
})
}
Loading
Loading