data-handlers

v1.0
Zero deps pt-BR first CPF · CNPJ · CEP · Telefone · RG Schema system Proxy fluente TypeScript Node ≥ 18

O que é #

data-handlers é uma biblioteca de normalização e validação de dados — sem dependências externas, com suporte nacional de primeira classe e um sistema de schemas para validar objetos inteiros.

Você provavelmente já escreveu código assim em vários lugares do seu sistema:

Sem data-handlers — espalhado, inconsistente
// Em algum lugar do projeto...
const nome = input.trim().toLowerCase().replace(/\s+/g, ' ')
  .split(' ').map(w => w[0].toUpperCase() + w.slice(1)).join(' ')

// Em outro lugar...
const cpf = raw.replace(/\D/g, '')
  .replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, '$1.$2.$3-$4')
if (!cpfRegex.test(cpf)) throw new Error('CPF inválido')

// E em outro...
const valor = new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(n)
Com data-handlers — uma API consistente
import { handlers } from 'data-handlers'

handlers.name.normalize('  joao   silva  ')   // 'Joao Silva'
handlers.cpf.normalize('11144477735')         // '111.444.777-35'
handlers.number.normalize(1234.5, { style: 'currency', currency: 'BRL' })  // 'R$ 1.234,50'

Handlers do core

  • name — Title Case + conectivos
  • number — Intl.NumberFormat
  • date — Intl.DateTimeFormat
  • password, url, uuid, any

Plugins nacionais

  • CPF, CNPJ, RG
  • CEP, Telefone
  • Slug, E-mail, Cor

API fluente

  • handlers.cpf.parse()
  • handlers.cpf.safe()
  • handlers.has() · .types

Schemas

  • Valida objetos inteiros
  • parse · safeParse
  • extend · pick · omit

Instalação #

Terminal
npm install data-handlers
# ou
pnpm add data-handlers
Requisito: Node.js 18 ou superior. Usa ESM (type: "module"). Autocomplete TypeScript funciona em JS puro via os arquivos .d.ts inclusos.

Conceito central #

A lib mantém um registry interno — um Map que associa um nome de tipo a uma função chamada handler.

Como o registry funciona internamente
// O registry nada mais é que um Map
registry.get('name')   // → nameHandler()
registry.get('number') // → numberHandler()
registry.get('cpf')    // → cpfHandler()   ← registrado pelo plugin

// Quando você chama:
handlers.name.normalize('joao')

// A lib faz internamente:
const handler = registry.get('name')
return handler('joao')  // → 'Joao'

Um handler é qualquer função (value, options?) => string que ou retorna o valor normalizado ou lança um TypeError descritivo se o valor for inválido. É tudo.

normalize() #

Função de nível superior para uso direto. Recebe um objeto com type, value e options opcionais. Lança se o valor for inválido.

normalize()
import { normalize } from 'data-handlers'

normalize({ type: 'name',   value: '  joao   silva  ' })
// → 'Joao Silva'

normalize({ type: 'cpf',    value: '11144477735' })
// → '111.444.777-35'

normalize({ type: 'number', value: 1234567.89, options: { style: 'currency', currency: 'BRL', locale: 'pt-BR' } })
// → 'R$ 1.234.567,89'

normalize({ type: 'date',   value: new Date(), options: { dateStyle: 'long', locale: 'pt-BR' } })
// → '3 de março de 2026'

// Valor inválido → lança
normalize({ type: 'cpf', value: '000.000.000-00' })
// → throws TypeError: [normalize:cpf] Invalid CPF. Received: 000.000.000-00
type é case-insensitive: 'name', 'Name' e 'NAME' são equivalentes. Espaços em volta também são ignorados.

validate() #

Mesma assinatura de normalize(), mas nunca lança. Retorna sempre um objeto { valid, value, error }.

validate()
import { validate } from 'data-handlers'

validate({ type: 'cpf', value: '11144477735' })
// → { valid: true, value: '111.444.777-35', error: null }

validate({ type: 'cpf', value: '000.000.000-00' })
// → { valid: false, value: null, error: '[normalize:cpf] Invalid CPF...' }

// Uso típico em formulários
const result = validate({ type: 'cpf', value: inputDoUsuario })

if (!result.valid) {
  mostrarErro(result.error)
} else {
  salvarNoBanco(result.value) // já formatado: '111.444.777-35'
}
PropriedadeQuando valid: trueQuando valid: false
validtruefalse
valueString normalizadanull
errornullMensagem do erro

Uso fluente com handlers #

Chamar normalize({ type: 'name', ... }) toda hora é verboso. O objeto handlers é um Proxy que cria accessors por tipo — você acessa o tipo como propriedade e chama o método.

handlers proxy
import { handlers } from 'data-handlers'

// Em vez de normalize({ type: 'name', value: '...' })
handlers.name.normalize('  joao  ')    // 'Joao'
handlers.cpf.normalize('11144477735') // '111.444.777-35'
handlers.date.normalize(new Date(), { dateStyle: 'short', locale: 'pt-BR' })

Cada handlers.<tipo> expõe quatro métodos:

MétodoComportamento
.normalize(v, opts)Normaliza — lança TypeError se inválido
.validate(v, opts)Nunca lança — retorna { valid, value, error }
.parse(v, opts)Alias de .normalize() — estilo Zod
.safe(v, opts)Alias de .validate() — estilo Zod
Os quatro métodos na prática
handlers.cpf.normalize('11144477735')  // '111.444.777-35'  — lança se inválido
handlers.cpf.parse('11144477735')     // '111.444.777-35'  — mesmo, alias Zod

handlers.cpf.validate('000.000.000-00') // { valid: false, value: null, error: '...' }
handlers.cpf.safe('000.000.000-00')    // idem, alias Zod
Qual usar? Use .parse() quando quiser falhar ruidosamente (ex: ao salvar no banco). Use .safe() quando precisar exibir feedback ao usuário sem travar o fluxo.

Introspecção #

handlers.has · handlers.types · handlers.$
handlers.has('cpf')       // true  — tipo existe no registry?
handlers.has('xyz')       // false

handlers.types
// ['name','number','date','cpf','cnpj','phone','cep','slug','email','rg','color']

// Namespace meta $ — mesmas funcionalidades, organizadas
handlers.$.has('cpf')     // true
handlers.$.types           // mesmo array acima

// Tentativa de atribuição direta lança TypeError
handlers.name = 'algo'    // → TypeError: Use register() to add handlers.

Handler: name #

Converte nomes para Title Case, com tratamento correto de acentos e conectivos.

namept-BR
handlers.name.normalize('MARIA SILVA')          // 'Maria Silva'
handlers.name.normalize('  joão   da   silva  ') // 'João da Silva'
handlers.name.normalize('pedro de souza')        // 'Pedro de Souza'
handlers.name.normalize('maria das dores')       // 'Maria das Dores'

Conectivos mantidos em minúsculo por padrão: de, da, do, das, dos, e, of, the, and, at, in, on

Opção lowerCaseWords
// Desabilitar completamente
handlers.name.normalize('joao de paula', { lowerCaseWords: [] })
// → 'Joao De Paula'

// Lista customizada
handlers.name.normalize('casa das flores', { lowerCaseWords: ['das'] })
// → 'Casa das Flores'

// Erros
handlers.name.normalize('')    // TypeError: [normalize:name] Expected non-empty string.
handlers.name.normalize(123)   // TypeError: [normalize:name] Expected non-empty string. Received: number

Handler: number #

Delega para Intl.NumberFormat. Locale padrão: pt-BR. Aceita qualquer opção do Intl.NumberFormatOptions.

number
handlers.number.normalize(1234567.89)
// → '1.234.567,89'  (locale padrão: pt-BR)

handlers.number.normalize(1234567.89, { locale: 'en-US' })
// → '1,234,567.89'

handlers.number.normalize(9.9, { style: 'currency', currency: 'BRL', locale: 'pt-BR' })
// → 'R$ 9,90'

handlers.number.normalize(9.9, { style: 'currency', currency: 'USD', locale: 'en-US' })
// → '$9.90'

handlers.number.normalize(0.753, { style: 'percent', maximumFractionDigits: 1 })
// → '75,3%'
Erros distintos: TypeError se o value não for do tipo number. RangeError se for NaN ou Infinity. Assim você sabe exatamente o que deu errado.
Erros do number
handlers.number.normalize('123')     // TypeError  — não é number
handlers.number.normalize(NaN)       // RangeError — NaN não é finito
handlers.number.normalize(Infinity)  // RangeError — Infinity não é finito

Handler: date #

Delega para Intl.DateTimeFormat. Locale padrão: pt-BR. Aceita três formatos de entrada.

date — três tipos de entrada
// Date object
handlers.date.normalize(new Date('2024-01-15'), { dateStyle: 'long', locale: 'pt-BR' })
// → '15 de janeiro de 2024'

// String ISO
handlers.date.normalize('2024-01-15T12:00:00', { dateStyle: 'short', locale: 'pt-BR' })
// → '15/01/2024'

// Timestamp Unix em ms (Date.now() ou valor salvo no banco)
handlers.date.normalize(1735689600000, { dateStyle: 'medium', locale: 'pt-BR' })
// → '1 de jan. de 2025'
Opções de dateStyle
const d = new Date(2024, 0, 15)
const o = { locale: 'pt-BR' }

handlers.date.normalize(d, { ...o, dateStyle: 'short'  }) // '15/01/2024'
handlers.date.normalize(d, { ...o, dateStyle: 'medium' }) // '15 de jan. de 2024'
handlers.date.normalize(d, { ...o, dateStyle: 'long'   }) // '15 de janeiro de 2024'
handlers.date.normalize(d, { ...o, dateStyle: 'full'   }) // 'segunda-feira, 15 de janeiro de 2024'

Plugin: cpf #

Valida usando o algoritmo oficial de dígito verificador e formata no padrão 000.000.000-00.

cpfpt-BR
handlers.cpf.normalize('11144477735')    // '111.444.777-35'
handlers.cpf.normalize('111.444.777-35') // '111.444.777-35'  (idempotente)

handlers.cpf.safe('00000000000')  // { valid: false, ... }  — sequência repetida
handlers.cpf.safe('11144477700')  // { valid: false, ... }  — dígito verificador errado
handlers.cpf.safe('11144477735')  // { valid: true,  value: '111.444.777-35', error: null }
Referência viva: a lista completa de tipos registrados está sempre disponível em runtime via handlers.types. Os plugins abaixo cobrem os principais — novos handlers podem estar disponíveis além dos documentados aqui.

Plugin: cnpj #

cnpjpt-BR
handlers.cnpj.normalize('11222333000181')     // '11.222.333/0001-81'
handlers.cnpj.normalize('11.222.333/0001-81') // '11.222.333/0001-81'

handlers.cnpj.safe('00000000000000')  // { valid: false, ... }

Plugin: cep #

ceppt-BR
handlers.cep.normalize('01310100')  // '01310-100'
handlers.cep.normalize('01310-100') // '01310-100'

handlers.cep.safe('0131010')  // { valid: false, ... }  — menos de 8 dígitos

Plugin: phone #

Suporta fixo (10 dígitos) e celular (11 dígitos).

phonept-BR
handlers.phone.normalize('11987654321') // '(11) 98765-4321'  — celular
handlers.phone.normalize('1134567890')  // '(11) 3456-7890'   — fixo
handlers.phone.normalize('(11) 98765-4321') // idempotente

handlers.phone.safe('123')  // { valid: false, ... }

Plugin: rg #

Formata no padrão SP 00.000.000-X. Suporta RGs de 7 a 9 dígitos e dígito verificador X.

rgpt-BR
handlers.rg.normalize('123456789')  // '12.345.678-9'
handlers.rg.normalize('12345678X')  // '12.345.678-X'  — dígito X válido

// Apenas dígitos, sem formatação
handlers.rg.normalize('123456789', { format: 'digits' }) // '123456789'

Plugin: slug #

slug
handlers.slug.normalize('Olá Mundo Legal!')    // 'ola-mundo-legal'
handlers.slug.normalize('Café & Chá')          // 'cafe-cha'
handlers.slug.normalize('  espaços   duplos ') // 'espacos-duplos'

// Separador customizado
handlers.slug.normalize('Meu Post Incrível', { separator: '_' })
// 'meu_post_incrivel'

Plugin: email #

Normaliza (trim + lowercase) e valida via RFC 5322 simplificado.

email
handlers.email.normalize('  JOAO@EMAIL.COM  ')  // 'joao@email.com'
handlers.email.normalize('USUARIO@EMAIL.COM')      // 'usuario@email.com'

handlers.email.safe('invalido')      // { valid: false, ... }
handlers.email.safe('sem@dominio')   // { valid: false, ... }
handlers.email.safe('ok@email.com')  // { valid: true, value: 'ok@email.com', ... }

Plugin: color #

Aceita #rgb, #rrggbb e rgb(r,g,b). Converte entre formatos.

color
// Hex shorthand expandido
handlers.color.normalize('#abc')     // '#aabbcc'
handlers.color.normalize('#FF0080')  // '#ff0080'

// RGB → hex
handlers.color.normalize('rgb(255, 0, 128)')  // '#ff0080'

// Formatos de saída
handlers.color.normalize('#ff0080', { format: 'rgb'       })  // 'rgb(255, 0, 128)'
handlers.color.normalize('#ff0080', { format: 'hex-upper' })  // '#FF0080'

Handler: password #

Valida senhas com regras configuráveis via options. Não normaliza — apenas valida e retorna o valor original se passar.

password — padrões default
handlers.password.normalize('Senha@123')  // 'Senha@123'
// padrão: minLength 8, requer maiúscula, minúscula e especial

handlers.password.safe('fraca')
// { valid: false, error: '[normalize:password] Password must be at least 8...' }

handlers.password.safe('semEspecial1')
// { valid: false, error: '[normalize:password] ...at least one special character' }
password — options configuráveis
handlers.password.normalize('SenhaSegura@123', {
  minLength:        12,      // padrão: 8
  requireUppercase: true,   // padrão: true
  requireLowercase: true,   // padrão: true
  requireSpecial:   true,   // padrão: true
  requireNumber:    true,   // padrão: false
})
password no schema
const signupSchema = schema({
  name:     'name',
  email:    'email',
  password: 'password',
  // com regras customizadas:
  // password: { type: 'password', options: { minLength: 12, requireNumber: true } }
})

Handler: url #

Valida e normaliza URLs. Coloca o host em lowercase, preserva path, query string e hash intactos.

url
handlers.url.normalize('https://example.com')          // 'https://example.com/'
handlers.url.normalize('HTTPS://EXAMPLE.COM/api/v1')    // 'https://example.com/api/v1'
handlers.url.normalize('https://x.com/path?foo=bar')    // preserva query

// Protocolo customizado
handlers.url.normalize('ftp://files.com', { protocols: ['ftp'] })  // 'ftp://files.com/'

// ftp sem permissão → lança
handlers.url.safe('ftp://files.com')  // { valid: false, ... }
handlers.url.safe('nao-e-url')        // { valid: false, ... }

Handler: uuid #

Valida formato UUID e normaliza capitalização. Suporta versões 1, 3, 4, 5 e 7.

uuid
// Normaliza para lowercase por padrão
handlers.uuid.normalize('550E8400-E29B-41D4-A716-446655440000')
// '550e8400-e29b-41d4-a716-446655440000'

// Uppercase se preferir
handlers.uuid.normalize('550e8400-e29b-41d4-a716-446655440000', { uppercase: true })
// '550E8400-E29B-41D4-A716-446655440000'

// Exige versão específica
handlers.uuid.normalize(id, { version: 4 })  // lança se não for v4

handlers.uuid.safe('nao-e-uuid')  // { valid: false, ... }

Handler: any #

Passa qualquer valor não-nulo sem normalizar. Essencial em schemas para campos que não têm um handler específico — metadata, configurações, campos livres.

any — sem restrição de tipo
// Qualquer valor não-nulo passa
handlers.any.normalize('texto')  // 'texto'
handlers.any.normalize(42)       // 42
handlers.any.normalize(true)    // true

// Null e undefined lançam sempre
handlers.any.safe(null)       // { valid: false, ... }
handlers.any.safe(undefined)  // { valid: false, ... }
any — com demandType
// Restringe o tipo primitivo aceito
handlers.any.normalize(42,   { demandType: 'number'  })  // 42
handlers.any.normalize(true, { demandType: 'boolean' })  // true
handlers.any.safe('oi',      { demandType: 'number'  })  // { valid: false, ... }

// No schema
const s = schema({
  tag:      'any',                                            // string, number, boolean...
  score:    { type: 'any', options: { demandType: 'number'  } },
  active:   { type: 'any', options: { demandType: 'boolean' } },
  metadata: { type: 'any', optional: true },
})
any — com transform
// Callback de transformação/validação customizada
handlers.any.normalize(5,    { transform: (v) => v * 2 })           // 10
handlers.any.normalize('oi', { transform: (v) => v.toUpperCase() })  // 'OI'

// demandType + transform: checagem de tipo roda antes do callback
handlers.any.normalize(150, {
  demandType: 'number',
  transform:  (v) => Math.min(100, v),  // clamp máximo 100
})  // 100

// O callback pode lançar para validações customizadas
handlers.any.normalize(value, {
  transform: (v) => {
    if (!Array.isArray(v))
      throw new TypeError('[normalize:any] Expected array.')
    return v.map(t => t.trim().toLowerCase())
  }
})

// No schema
const s = schema({
  score: {
    type: 'any',
    options: {
      demandType: 'number',
      transform:  (v) => Math.max(0, Math.min(100, v)),  // clamp 0–100
    }
  },
  tags: {
    type: 'any',
    options: {
      transform: (v) => Array.isArray(v) ? v.map(t => t.toLowerCase()) : [v],
    }
  },
})
Dica: any também aceita objetos e arrays. O transform é a saída perfeita pra validações e transformações que não cabem num handler dedicado — sem precisar criar um tipo novo no registry.

Schemas: validar objetos inteiros #

Quando você tem um formulário ou payload com múltiplos campos, fazer a validação campo por campo na mão fica trabalhoso. O sistema de schemas resolve isso de uma vez.

Definindo um schema
import { schema } from 'data-handlers'

const cadastroSchema = schema({
  name:     'name',    // forma curta: só o tipo
  document: 'cpf',
  phone:    'phone',
  email:    'email',
})

parse() e safeParse() #

parse() — lança SchemaError se inválido
const dados = cadastroSchema.parse({
  name:     'JOAO SILVA',
  document: '11144477735',
  phone:    '11987654321',
  email:    'JOAOsilVa95@Email.com',
})

// dados →
// {
//   name:     'Joao Silva',
//   document: '111.444.777-35',
//   phone:    '(11) 98765-4321',
//   email:    'joaosilva95@email.com'
// }
safeParse() — nunca lança
const result = cadastroSchema.safeParse(req.body)

if (!result.success) {
  return res.status(400).json({ errors: result.errors })
  // errors: {
  //   name:     '[normalize:name] Expected non-empty string...',
  //   document: '[normalize:cpf] Invalid CPF...',
  // }
}

await salvarUsuario(result.data) // já normalizado

Opções de campo #

Além da forma curta (só o tipo como string), cada campo aceita um objeto com configurações:

Forma completa de um campo
const pedidoSchema = schema({
  // Forma curta
  customerName: 'name',

  // optional: null/undefined passam sem erro
  coupon: { type: 'slug', optional: true },

  // default: valor usado quando o campo vier undefined
  country: { type: 'slug', default: 'brasil' },

  // options: repassadas ao handler
  total: {
    type:    'number',
    options: { style: 'currency', currency: 'BRL', locale: 'pt-BR' }
  },

  // label: nome legível nas mensagens de erro
  doc: {
    type:  'cpf',
    label: 'CPF do cliente',
  },
})
OpçãoTipoDescrição
typestringTipo registrado (obrigatório)
optionalbooleanSe true, null/undefined passam sem erro
defaultanyValor usado quando o campo for undefined
optionsobjectRepassado ao handler como segundo argumento
labelstringNome legível nas mensagens de erro

Utilitários de schema #

Todos retornam novos schemas — o original nunca é modificado.

extend · pick · omit · partial
const userSchema = schema({ name: 'name', email: 'email', document: 'cpf' })

// .partial() — todos os campos viram opcionais
const updateSchema = userSchema.partial()
updateSchema.safeParse({}) // success: true — todos campos omitidos ok

// .pick() — só os campos especificados
const loginSchema = userSchema.pick('email', 'document')

// .omit() — remove campos
const publicUser = userSchema.omit('document') // sem CPF na resposta pública

// .extend() — adiciona campos (não altera o original)
const fullSchema = userSchema.extend({ phone: 'phone', cep: 'cep' })

// Padrão prático: um schema base, três variações
const createUser = userSchema                  // todos obrigatórios
const patchUser  = userSchema.partial()        // todos opcionais (PATCH)
const publicUser2 = userSchema.omit('document') // sem CPF no response

SchemaError #

Quando .parse() falha, lança uma instância de SchemaError com o mapa completo de erros.

Capturando SchemaError
import { schema, SchemaError } from 'data-handlers'

try {
  cadastroSchema.parse({ name: '', document: 'invalido', phone: '123' })
} catch (err) {
  if (err instanceof SchemaError) {
    console.log(err.errors)
    // {
    //   name:     '[normalize:name] Expected non-empty string...',
    //   document: '[normalize:cpf] Invalid CPF...',
    //   phone:    '[normalize:phone] Expected 10 or 11-digit...'
    // }
  }
}
Todos os erros de uma vez: o schema não para no primeiro erro. Ele processa todos os campos e retorna o mapa completo — assim você pode exibir todos os problemas para o usuário de uma só vez.

register() — handlers customizados #

A lib foi feita para ser estendida. Um handler é qualquer função (value, options?) => string que lança TypeError se o valor for inválido.

Criando e usando um handler customizado
import { register, handlers } from 'data-handlers'

register('placa', (value) => {
  const clean = String(value).toUpperCase().replace(/[^A-Z0-9]/g, '')

  if (!/^[A-Z]{3}[0-9]{4}$/.test(clean) && !/^[A-Z]{3}[0-9][A-Z][0-9]{2}$/.test(clean)) {
    throw new TypeError(`[normalize:placa] Placa inválida. Recebido: ${value}`)
  }

  return `${clean.slice(0, 3)}-${clean.slice(3)}`
})

handlers.placa.normalize('ABC1234')  // 'ABC-1234'
handlers.placa.normalize('ABC1D23')  // 'ABC-1D23'  (Mercosul)
handlers.placa.safe('INVALIDO')     // { valid: false, ... }

// E funciona em schemas também
const veiculoSchema = schema({ owner: 'name', plate: 'placa' })

registerAliases() — múltiplos nomes #

registerAliases()
import { registerAliases, handlers } from 'data-handlers'

// O tipo 'name' já existe — cria aliases que apontam pro mesmo handler
registerAliases('name', 'nome', 'fullName', 'fullname', 'nomeCompleto')

handlers.nome.normalize('joao silva')          // 'Joao Silva'
handlers.nomeCompleto.normalize('joao silva')   // 'Joao Silva'

// Útil para suporte pt-BR / en-US paralelo no mesmo sistema
registerAliases('cpf', 'document', 'taxId')
handlers.document.safe('11144477735') // { valid: true, value: '111.444.777-35' }

createPlugin() — para pacotes externos #

Alias semântico de register(). Use quando for publicar um handler como pacote npm separado (data-handlers-pix, data-handlers-nfe, etc.).

Estrutura de um plugin externo
// data-handlers-pix/index.js
import { createPlugin } from 'data-handlers'

const pixHandler = (value) => {
  // Chave Pix pode ser CPF, CNPJ, telefone, email ou UUID aleatório
  const clean = String(value).trim()
  if (!isValidPixKey(clean)) {
    throw new TypeError(`[normalize:pix] Chave Pix inválida. Recebido: ${value}`)
  }
  return clean
}

createPlugin('pix', pixHandler)

// Quem instalar e importar o pacote terá handlers.pix disponível automaticamente

Padrão: endpoint REST #

Express — POST /usuarios
import { schema, SchemaError } from 'data-handlers'

const cadastroSchema = schema({
  name:     'name',
  document: 'cpf',
  phone:    { type: 'phone', optional: true },
  email:    'email',
  zipCode:  { type: 'cep', label: 'CEP' },
})

app.post('/usuarios', (req, res) => {
  const result = cadastroSchema.safeParse(req.body)

  if (!result.success) {
    return res.status(400).json({
      message: 'Dados inválidos',
      errors:  result.errors,
    })
  }

  // result.data está completamente normalizado e pronto pro banco
  db.users.create(result.data)
  res.status(201).json(result.data)
})

Padrão: validação de formulário #

Feedback em tempo real no input
import { handlers } from 'data-handlers'

inputCPF.addEventListener('input', (e) => {
  const { valid, value, error } = handlers.cpf.safe(e.target.value)

  if (valid) {
    e.target.value = value          // aplica a máscara
    campoErro.textContent = ''
    inputCPF.classList.remove('error')
  } else {
    campoErro.textContent = 'CPF inválido'
    inputCPF.classList.add('error')
  }
})

Padrão: formatar antes de exibir #

Formatar dados vindos do banco
import { handlers } from 'data-handlers'

function formatarPerfil(usuario) {
  return {
    ...usuario,
    name:      handlers.name.normalize(usuario.name),
    document:  handlers.cpf.normalize(usuario.document),
    phone:     handlers.phone.normalize(usuario.phone),
    createdAt: handlers.date.normalize(usuario.createdAt, {
      dateStyle: 'long',
      locale:    'pt-BR',
    }),
    balance: handlers.number.normalize(usuario.balance, {
      style:    'currency',
      currency: 'BRL',
      locale:   'pt-BR',
    }),
  }
}

Padrão: PATCH parcial #

Um schema base, várias operações
const userSchema   = schema({ name: 'name', email: 'email', phone: 'phone' })
const updateSchema = userSchema.partial()  // todos os campos viram opcionais

app.patch('/usuarios/:id', (req, res) => {
  const result = updateSchema.safeParse(req.body)

  if (!result.success) {
    return res.status(400).json({ errors: result.errors })
  }

  // Só atualiza os campos que vieram — os outros ficam como estão
  db.users.update(req.params.id, result.data)
  res.json(result.data)
})

Bônus: data-handlers/serve #

A lib inclui um wrapper HTTP para Node.js puro — sem Express, sem Fastify. A API é intencionalmente parecida com o Bun.serve(), então quem usa Bun vai se sentir em casa, e quem usa Node.js puro não precisa aprender um framework novo só pra ter roteamento básico.

Node.js >= 18 apenas. Usa Web APIs nativas (Request, Response, URL). Não é necessário com Bun, Express, Fastify ou similares — esses já têm roteamento próprio.
Servidor básico
import { serve } from 'data-handlers/serve'
import { schema } from 'data-handlers'

const userSchema = schema({
  name:     'name',
  email:    'email',
  phone:    'phone',
  document: 'cpf',
  password: 'password',
})

const users = []

serve({
  port: 3000,

  routes: {
    '/': () => Response.json({ message: 'Welcome to the backend.' }),

    '/users': {
      GET: () => Response.json(users),

      POST: async (req) => {
        const result = userSchema.safeParse(await req.json())
        if (!result.success)
          return Response.json(result.errors, { status: 400 })

        users.push(result.data)
        return Response.json(result.data, { status: 201 })
      }
    },

    '/users/:id': {
      GET: (req) => {
        const user = users.find(u => u.id === req.params.id)
        if (!user) return new Response('Not Found', { status: 404 })
        return Response.json(user)
      },

      DELETE: (req) => {
        const i = users.findIndex(u => u.id === req.params.id)
        if (i === -1) return new Response('Not Found', { status: 404 })
        users.splice(i, 1)
        return new Response(null, { status: 204 })
      }
    }
  },

  error: (err) => Response.json({ error: err.message }, { status: 500 })
})

Cada rota aceita um handler direto (qualquer método) ou um objeto com os métodos separados. Params de URL ficam em req.params:

Rotas com params e wildcard
import { serve } from 'data-handlers/serve'

serve({
  port: 3000,
  routes: {
    // Handler direto — qualquer método
    '/': (req) => Response.json({ method: req.method }),

    // Params de URL
    '/posts/:slug': {
      GET: (req) => Response.json({ slug: req.params.slug }),
    },

    // Múltiplos params
    '/users/:id/posts/:postId': {
      GET: (req) => Response.json(req.params),
      // req.params → { id: '123', postId: '456' }
    },

    // Wildcard — captura qualquer rota não mapeada
    '/*': () => Response.json({ error: 'Not found' }, { status: 404 }),
  }
})
fetch fallback e error handler
serve({
  port: 3000,
  routes: { /* ... */ },

  // fetch: chamado quando nenhuma rota casa
  fetch: (req) => {
    const url = new URL(req.url)
    return Response.json({ path: url.pathname, message: 'rota não encontrada' }, { status: 404 })
  },

  // error: captura qualquer erro não tratado nos handlers
  error: (err) => {
    console.error(err)
    return Response.json({ error: 'Internal Server Error' }, { status: 500 })
  }
})
O objeto retornado por serve() é um http.Server nativo do Node — dá pra fazer tudo que você faria normalmente, tipo server.close() em testes. Ele também expõe server.url com a URL base do servidor.

Cheatsheet #

Referência rápida — tudo em um lugar
import { normalize, validate, handlers, schema, SchemaError,
         register, registerAliases, createPlugin } from 'data-handlers'

// ── Funções base ─────────────────────────────────────────────────────────
normalize({ type, value, options })   // formata — lança se inválido
validate({ type, value, options })    // { valid, value, error } — nunca lança

// ── handlers proxy ───────────────────────────────────────────────────────
handlers.<tipo>.normalize(v, opts)   // lança se inválido
handlers.<tipo>.validate(v, opts)    // nunca lança
handlers.<tipo>.parse(v, opts)      // alias de normalize
handlers.<tipo>.safe(v, opts)       // alias de validate

handlers.has('cpf')                 // true/false
handlers.types                       // ['name','number','date','password','url','uuid','any','cpf',...]
handlers.$.has('cpf')              // meta namespace
handlers.$.types

// ── Schemas ──────────────────────────────────────────────────────────────
const s = schema({ name: 'name', doc: 'cpf', pass: 'password', tag: 'any', phone: { type: 'phone', optional: true } })

s.parse(obj)        // lança SchemaError com .errors se inválido
s.safeParse(obj)    // { success, data, errors } — nunca lança
s.partial()         // todos os campos opcionais
s.pick('name')      // só esses campos
s.omit('phone')    // sem esse campo
s.extend({ x: 'slug' }) // adiciona campos

// ── Extensão ─────────────────────────────────────────────────────────────
register('tipo', handler)                 // registra handler customizado
registerAliases('name', 'nome', 'fullName') // múltiplos nomes pro mesmo handler
createPlugin('tipo', handler)              // alias de register()