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:
// 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)
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 #
npm install data-handlers
# ou
pnpm add data-handlers
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.
// 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.
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
'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 }.
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' }
| Propriedade | Quando valid: true | Quando valid: false |
|---|---|---|
| valid | true | false |
| value | String normalizada | null |
| error | null | Mensagem 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.
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étodo | Comportamento |
|---|---|
| .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 |
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
.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('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.
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
// 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.
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%'
TypeError se o value não for do tipo number. RangeError se for NaN ou Infinity. Assim você sabe exatamente o que deu errado.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 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'
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.
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 }
handlers.types. Os plugins abaixo cobrem os principais — novos handlers podem estar disponíveis além dos documentados aqui.Plugin: cnpj #
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 #
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).
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.
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 #
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.
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.
// 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.
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' }
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 })
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.
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.
// 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.
// 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, ... }
// 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 }, })
// 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], } }, })
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.
import { schema } from 'data-handlers' const cadastroSchema = schema({ name: 'name', // forma curta: só o tipo document: 'cpf', phone: 'phone', email: 'email', })
parse() e safeParse() #
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' // }
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:
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ção | Tipo | Descrição |
|---|---|---|
| type | string | Tipo registrado (obrigatório) |
| optional | boolean | Se true, null/undefined passam sem erro |
| default | any | Valor usado quando o campo for undefined |
| options | object | Repassado ao handler como segundo argumento |
| label | string | Nome legível nas mensagens de erro |
Utilitários de schema #
Todos retornam novos schemas — o original nunca é modificado.
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.
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...' // } } }
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.
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 #
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.).
// 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 #
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 #
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 #
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 #
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.
Request, Response, URL). Não é necessário com Bun, Express, Fastify ou similares — esses já têm roteamento próprio.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:
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 }), } })
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 })
}
})
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 #
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()