O Avro no Node.js é uma opção de serialização binária orientada por schema, muito usada em pipelines de dados e eventos Kafka. Diferente de JSON, o formato não repete nomes de campos em cada mensagem. O produtor e o consumidor compartilham um contrato, reduzindo tamanho e permitindo evolução controlada.
Avro é especialmente útil quando várias aplicações precisam trocar eventos por anos, com produtores e consumidores atualizados em momentos diferentes. O formato define regras para records, unions, arrays, enums, defaults e compatibilidade entre writer schema e reader schema.
Neste guia, você aprenderá a criar schemas Avro, validar e codificar dados no Node.js, trabalhar com tipos long, unions e enums, integrar com Kafka e Schema Registry, testar evolução e evitar mudanças incompatíveis.
O que é Apache Avro?
Apache Avro é um sistema de serialização de dados que usa schemas JSON para descrever estruturas binárias. A especificação oficial do Avro define tipos primitivos, tipos complexos, resolução de schemas e formatos de arquivo e RPC.
Um record simples:
{
"type": "record",
"name": "OrderCreated",
"namespace": "com.codigofacil.orders",
"fields": [
{ "name": "orderId", "type": "string" },
{ "name": "totalCents", "type": "long" },
{ "name": "occurredAt", "type": "string" }
]
}Por que Avro é compacto?
O schema informa a ordem e o tipo dos campos. A mensagem binária contém apenas os valores codificados. Em JSON, cada evento repete orderId, totalCents e outros nomes.
O ganho depende do payload, mas é relevante em tópicos de alto volume. O custo é precisar gerenciar schemas e ferramentas de inspeção.
Biblioteca avsc
Uma biblioteca popular para Node.js é avsc:
npm install avscimport avro from 'avsc';
const type = avro.Type.forSchema({
type: 'record',
name: 'OrderCreated',
fields: [
{ name: 'orderId', type: 'string' },
{ name: 'totalCents', type: 'long' },
{ name: 'occurredAt', type: 'string' }
]
});Validando dados
const event = {
orderId: 'order-42',
totalCents: 15990,
occurredAt: new Date().toISOString()
};
if (!type.isValid(event)) {
throw new Error('Evento incompatível com schema Avro');
}Valide antes de publicar para falhar perto da origem do erro.
Codificando
const buffer = type.toBuffer(event);O resultado é um Buffer que pode ser enviado ao Kafka, salvo em arquivo ou transmitido por outro protocolo.
Decodificando
const decoded = type.fromBuffer(buffer);
console.log(decoded.orderId);Quando o writer e reader usam schemas diferentes, a resolução precisa conhecer os dois contratos.
Tipos primitivos
Avro possui:
null;boolean;int;long;float;double;bytes;string.
Escolha o tipo pelo domínio. Valores monetários devem usar inteiro em centavos ou decimal lógico, não float.
O problema de long no JavaScript
JavaScript usa números IEEE-754 e não representa com segurança todos os inteiros de 64 bits. Se um long pode ultrapassar Number.MAX_SAFE_INTEGER, configure a biblioteca para BigInt ou representação customizada.
const value = BigInt('9223372036854775807');Teste serialização e JSON, porque JSON.stringify não aceita BigInt por padrão.
Campos opcionais
Avro usa union:
{
"name": "couponCode",
"type": ["null", "string"],
"default": null
}A ordem importa: o default precisa corresponder ao primeiro tipo da union segundo as regras aplicáveis.
Defaults e evolução
Adicionar um campo com default permite que leitores novos processem mensagens antigas:
{
"name": "currency",
"type": "string",
"default": "BRL"
}O default não significa que o produtor omite o campo durante codificação em todos os casos; ele participa principalmente da resolução de schema.
Enums
{
"name": "status",
"type": {
"type": "enum",
"name": "OrderStatus",
"symbols": ["CREATED", "PAID", "CANCELLED"]
}
}Adicionar símbolos pode quebrar consumidores antigos que não conhecem o novo valor. Use default de enum quando suportado e teste compatibilidade.
Arrays
{
"name": "items",
"type": {
"type": "array",
"items": {
"type": "record",
"name": "OrderItem",
"fields": [
{ "name": "productId", "type": "string" },
{ "name": "quantity", "type": "int" }
]
}
}
}Maps
{
"name": "metadata",
"type": {
"type": "map",
"values": "string"
},
"default": {}
}Maps sempre possuem chaves string. Não use metadata como depósito sem governança.
Logical types
Avro define tipos lógicos sobre tipos primitivos, como:
- date;
- time-millis;
- timestamp-millis;
- uuid;
- decimal.
Confirme suporte da biblioteca Node.js. Uma ferramenta pode tratar o valor apenas como número ou bytes sem conversão automática.
Datas
Para interoperabilidade simples, muitos eventos usam timestamp ISO em string:
{
"name": "occurredAt",
"type": "string"
}Para eficiência, timestamp-millis usa long. Documente timezone UTC e precisão.
Decimal
Valores decimais precisos podem usar logical type decimal sobre bytes ou fixed:
{
"name": "amount",
"type": {
"type": "bytes",
"logicalType": "decimal",
"precision": 12,
"scale": 2
}
}Verifique suporte da biblioteca e nunca converta por float sem cuidado.
Named types
Records, enums e fixed possuem nome e namespace. Isso evita repetir schemas e permite referências:
{
"type": "record",
"name": "Customer",
"namespace": "com.codigofacil.customers",
"fields": []
}Mudar o nome completo pode ser breaking.
Aliases
Aliases ajudam a resolver renomeações em casos suportados:
{
"name": "customerId",
"aliases": ["clientId"],
"type": "string"
}Não dependa de aliases sem testar todos os consumidores.
Writer e reader schema
O writer schema descreve como a mensagem foi gravada. O reader schema descreve como o consumidor quer lê-la. Avro resolve diferenças compatíveis:
- campos extras do writer podem ser ignorados;
- campos ausentes no writer podem usar default do reader;
- alguns tipos numéricos podem ser promovidos;
- nomes e aliases precisam corresponder.
Schema Registry
No Kafka, o wire format costuma incluir um schema ID. O consumidor busca o writer schema no registry. Veja Schema Registry no Node.js.
KafkaJS
Depois de codificar:
await producer.send({
topic: 'orders.created.v1',
messages: [{
key: event.orderId,
value: buffer
}]
});Consulte Kafka com Node.js.
Envelope de evento
Um schema consistente pode incluir:
{
"eventId": "evt-123",
"eventType": "order.created",
"eventVersion": 1,
"occurredAt": "2026-09-14T18:00:00Z",
"correlationId": "corr-42",
"data": {}
}O envelope ajuda observabilidade e idempotência. Veja Idempotência em APIs Node.js.
Compatibilidade backward
Um reader novo deve ler dados antigos. Adicionar campo com default costuma ser compatível. Remover campo do reader pode ser compatível se ele simplesmente ignora dados antigos.
Compatibilidade forward
Um reader antigo deve ler dados novos. Adicionar campo ao writer tende a funcionar porque o reader ignora o campo desconhecido.
Mudanças incompatíveis
- renomear sem alias;
- trocar string por int;
- remover default necessário;
- mudar record name;
- alterar semântica mantendo o mesmo campo;
- reutilizar enum com significado diferente.
Teste com fixtures
Salve bytes de versões anteriores:
test('schema atual lê evento v1', () => {
const buffer = readFileSync('fixtures/order-created-v1.avro');
const event = currentType.fromBuffer(buffer);
assert.equal(event.currency, 'BRL');
});Fixtures protegem compatibilidade real.
Teste de round trip
const encoded = type.toBuffer(event);
const decoded = type.fromBuffer(encoded);
assert.deepEqual(decoded, event);Round trip não substitui testes entre versões diferentes.
Geração de tipos TypeScript
É possível gerar interfaces a partir de schemas ou gerar schemas a partir de tipos, mas evite duas fontes de verdade. Escolha o contrato principal e valide o código gerado no CI.
TypeScript desaparece em runtime; Avro continua validando dados reais.
Performance
Compile schemas uma vez no startup:
const orderCreatedType = avro.Type.forSchema(schema);Não reconstrua o tipo para cada mensagem. Meça encoding, decoding, tamanho e garbage collection.
Segurança
- Limite tamanho de payload.
- Não aceite schemas arbitrários de usuários.
- Restrinja registro no registry.
- Valide logical types.
- Proteja dados sensíveis.
- Monitore falhas de decode.
Observabilidade
Registre schema ID, event type e versão, mas não o payload inteiro. Meça:
- falhas de encode;
- falhas de decode;
- schema desconhecido;
- latência;
- tamanho de mensagem;
- incompatibilidade no pipeline.
Consulte Métricas Prometheus no Node.js.
Avro versus JSON
Avro é mais compacto e possui evolução formal. JSON é mais fácil de inspecionar e integrar. Para APIs HTTP humanas, JSON costuma ser melhor. Para eventos de alto volume, Avro pode reduzir custo e erro.
Avro versus Protobuf
Avro é muito alinhado a Kafka e resolução entre writer e reader. Protobuf usa arquivos .proto, field numbers e geração de código. O próximo artigo detalha Protobuf.
Erros comuns
- long como Number: perde precisão.
- Campo opcional sem union: validação falha.
- Default incorreto: evolução quebra.
- Schema compilado por mensagem: performance cai.
- Sem fixtures antigas: compatibilidade não é testada.
- Alterar sem registry: consumidores quebram.
- BigInt serializado em JSON: gera erro.
- Schema sem ownership: contratos se degradam.
Conclusão
O Avro no Node.js fornece serialização compacta e evolução de schemas para eventos duráveis. Records, unions, defaults e resolução entre writer e reader permitem atualizar produtores e consumidores em momentos diferentes.
Compile schemas uma vez, trate long e decimal corretamente, registre contratos e teste bytes antigos. Com Schema Registry e Kafka, Avro cria uma base sólida para mensagens pequenas, validadas e compatíveis ao longo do tempo.




