Event-driven architecturen zijn niet meer weg te denken uit moderne backends, en Kafka integratie in Node.js is een van de meest gevraagde skills voor developers die schaalbare systemen bouwen. Of je nu audit logs streamt, microservices ontkoppelt of realtime analytics bouwt: Kafka is vaak de juiste tool.
In deze handleiding leer je stap voor stap hoe je Kafka koppelt aan een Node.js applicatie met KafkaJS. Je bouwt een producer, een consumer group, en leert hoe je omgaat met retries, offsets en fouten in productie.
Waarom Kafka en wanneer gebruik je het?
Kafka is een distributed commit log. In plaats van berichten direct af te leveren en weg te gooien, bewaart Kafka events in een geordende stream per topic. Meerdere consumers kunnen diezelfde stream onafhankelijk lezen, elk op hun eigen tempo.
Dat maakt Kafka geschikt voor:
- Event sourcing, historische events bewaren als source of truth
- Log aggregation, logs van honderden services centraliseren
- Stream processing, realtime data verwerken en transformeren
- Service decoupling, microservices communiceren asynchroon
Voor eenvoudige taakverwerking blijft een queue zoals BullMQ prima. Lees onze gids over background jobs en workers als je twijfelt tussen een queue of een event log.
Stap 1: Kafka lokaal draaien met Docker
Voordat je Node.js code schrijft, heb je een Kafka-broker nodig. De makkelijkste weg is Docker Compose. Maak een docker-compose.yml aan:
services:
kafka:
image: bitnami/kafka:3.7
ports:
- "9092:9092"
environment:
KAFKA_CFG_NODE_ID: 0
KAFKA_CFG_PROCESS_ROLES: controller,broker
KAFKA_CFG_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093
KAFKA_CFG_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
KAFKA_CFG_CONTROLLER_LISTENER_NAMES: CONTROLLER
KAFKA_CFG_CONTROLLER_QUORUM_VOTERS: 0@localhost:9093
KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT
Start het geheel met docker compose up -d. Je hebt nu een werkende broker op localhost:9092.
Voor een lichtere setup kun je ook Redpanda overwegen: een Kafka-compatibele broker zonder Zookeeper of JVM.
Stap 2: KafkaJS installeren en verbinden
KafkaJS is de go-to library voor Kafka in Node.js. Pure JavaScript, geen native dependencies, en uitstekende documentatie.
npm install kafkajs
Maak een centrale kafka.ts voor de client-configuratie:
import { Kafka, logLevel } from 'kafkajs'
export const kafka = new Kafka({
clientId: 'orders-service',
brokers: (process.env.KAFKA_BROKERS ?? 'localhost:9092').split(','),
logLevel: logLevel.INFO,
retry: {
initialRetryTime: 300,
retries: 8,
},
})
De clientId helpt je in de Kafka-logs om te zien welke service wat doet. In productie draai je meerdere brokers, dus de brokers-array komt uit een environment variable.
Stap 3: Een producer bouwen
Een producer publiceert berichten naar een topic. Hier is een minimale order-producer:
import { kafka } from './kafka'
const producer = kafka.producer({
allowAutoTopicCreation: false,
idempotent: true,
})
export async function publishOrderCreated(order: {
id: string
userId: string
total: number
}) {
await producer.send({
topic: 'orders.created',
messages: [
{
key: order.id,
value: JSON.stringify(order),
headers: {
'content-type': 'application/json',
'schema-version': '1',
},
},
],
})
}
export async function connectProducer() {
await producer.connect()
}
export async function disconnectProducer() {
await producer.disconnect()
}
Drie dingen zijn belangrijk hier:
idempotent: truezorgt dat een retry geen duplicaat produceert.keybepaalt naar welke partitie het bericht gaat. Dezelfde key belandt altijd op dezelfde partitie, wat ordening binnen een key garandeert.headersgebruik je voor metadata zoals schema-versies of tracing IDs.
Stap 4: Een consumer met consumer group
Consumers leven in een consumer group. Kafka verdeelt partities over de consumers in de groep, zodat je horizontaal kunt schalen.
import { kafka } from './kafka'
const consumer = kafka.consumer({
groupId: 'orders-email-worker',
sessionTimeout: 30000,
heartbeatInterval: 3000,
})
export async function startOrderEmailWorker() {
await consumer.connect()
await consumer.subscribe({
topic: 'orders.created',
fromBeginning: false,
})
await consumer.run({
eachMessage: async ({ topic, partition, message }) => {
const order = JSON.parse(message.value!.toString())
try {
await sendOrderConfirmationEmail(order)
} catch (error) {
console.error('Failed to handle order', {
topic,
partition,
offset: message.offset,
error,
})
throw error
}
},
})
}
Zolang eachMessage succesvol afrondt, commit KafkaJS automatisch de offset. Gooi je een error, dan blijft het bericht beschikbaar voor een retry.
Wil je meerdere worker-instances draaien? Gebruik hetzelfde groupId. Combineer dit met clustering en scaling om de throughput te verhogen.
Stap 5: Foutafhandeling en dead letter topics
In productie gaat er altijd iets mis: een externe API is traag, een bericht is corrupt, of een database staat even plat. Een robuuste consumer scheidt transient fouten van permanente fouten.
const MAX_RETRIES = 3
async function handleMessage(payload: EachMessagePayload) {
const retryCount = Number(payload.message.headers?.['retry-count'] ?? 0)
try {
await processOrder(JSON.parse(payload.message.value!.toString()))
} catch (error) {
if (isTransient(error) && retryCount < MAX_RETRIES) {
throw error
}
await sendToDeadLetter(payload, error)
}
}
Een dead letter topic (bijvoorbeeld orders.created.dlq) verzamelt berichten die niet verwerkt kunnen worden. Daar kun je handmatig of met een replay-tool naar kijken. Meer over deze aanpak lees je in onze gids over error handling op schaal.
Stap 6: Graceful shutdown
Een consumer die abrupt stopt, kan een bericht half verwerken. Handel SIGTERM netjes af:
async function shutdown() {
console.log('Shutting down...')
await consumer.disconnect()
await producer.disconnect()
process.exit(0)
}
process.on('SIGTERM', shutdown)
process.on('SIGINT', shutdown)
Kubernetes stuurt SIGTERM bij een rolling deploy. Door de consumer netjes te disconnecten, finalizeert Kafka de huidige batch en laat de groep rebalancen zonder berichten te verliezen.
Stap 7: Berichten valideren
Events zijn contracten tussen services. Eén foutief bericht kan een hele keten breken. Valideer daarom elk inkomend bericht strikt.
import { z } from 'zod'
const OrderCreatedSchema = z.object({
id: z.string().uuid(),
userId: z.string().uuid(),
total: z.number().positive(),
createdAt: z.string().datetime(),
})
const raw = JSON.parse(message.value!.toString())
const order = OrderCreatedSchema.parse(raw)
Faalt de validatie, stuur dan direct naar de dead letter topic, een retry gaat het probleem niet oplossen. Lees meer over deze aanpak in onze gids over validatie met Zod.
Best practices voor Kafka in Node.js
Een paar regels die je veel tijd besparen:
- Gebruik compressie, zet
compression: CompressionTypes.GZIPop de producer voor grote payloads - Partitiestrategie, kies je key bewust; een slechte key veroorzaakt hot partitions
- Monitor consumer lag, de
lagtussen de nieuwste offset en de gecommitte offset is je belangrijkste metric - Batch waar mogelijk, gebruik
eachBatchin plaats vaneachMessagevoor throughput-gevoelige workloads - Schema evolution, versie je events vanaf dag één, bijvoorbeeld met een
schema-versionheader
Voor meer context over performance-overwegingen, bekijk onze performance tuning gids voor Node.js.
Integreren met een HTTP API
Een typisch patroon: je Express API publiceert events na een succesvolle database-write. Zo koppel je de producer aan je REST-laag:
app.post('/orders', async (req, res) => {
const order = await db.orders.create(req.body)
await publishOrderCreated(order)
res.status(201).json(order)
})
Let op: als de database-write slaagt maar de Kafka-publish faalt, loopt je event stream uit de pas. Voor kritieke flows gebruik je het transactional outbox pattern: je schrijft het event samen met de order in één database-transactie, en een aparte worker pusht het naar Kafka.
Voor meer over het opzetten van betrouwbare endpoints, lees onze REST API design best practices.
Veelgestelde vragen
Wat is het verschil tussen Kafka en een message queue zoals RabbitMQ?
Kafka is een distributed log waarbij berichten worden bewaard en meerdere consumers dezelfde stream kunnen lezen. RabbitMQ is een traditionele message broker waarbij berichten meestal na consumptie verdwijnen. Kafka schaalt beter voor event streaming en hoge throughput.
Welke Kafka-client moet ik gebruiken in Node.js?
KafkaJS is de meest gebruikte en actief onderhouden pure JavaScript-client. Voor extreme performance kun je node-rdkafka overwegen, maar die vereist native bindings. Voor de meeste projecten is KafkaJS de beste keuze.
Hoe garandeer ik dat berichten niet verloren gaan?
Gebruik aan de producer-kant acks: 'all' en idempotente writes. Aan de consumer-kant commit je offsets pas nadat een bericht succesvol is verwerkt. Combineer dit met retries en een dead letter topic voor falende berichten.
Wat is een consumer group in Kafka?
Een consumer group is een set consumers die samen de berichten van een topic verwerken. Kafka verdeelt de partities over de consumers in de groep, zodat elk bericht precies één keer wordt verwerkt binnen die groep.
Kan ik Kafka lokaal draaien voor development?
Ja, met Docker Compose kun je een lokale Kafka-broker draaien, bijvoorbeeld met de Confluent- of Bitnami-images. Voor een kleinere footprint is Redpanda een Kafka-compatibel alternatief dat lichter is om lokaal te draaien.
Conclusie
Kafka integreren in Node.js hoeft niet ingewikkeld te zijn. Met KafkaJS, een goede producer-configuratie, consumer groups en een dead letter topic heb je een robuuste basis voor event-driven architecturen.
Begin klein: één topic, één producer, één consumer. Breid uit naarmate je meer events toevoegt. En vergeet niet om consumer lag te monitoren, dat is de vroegste waarschuwing dat iets niet goed loopt in productie.