Overview

Build event-driven systems with Kafka. Supports producers, consumers, and admin operations.

Features

  • Producer with compression
  • Consumer subscriptions
  • Topic management
  • Batch publishing

Installation

npx hanma add kafka-service --framework nest

Dependencies

kafkajs

Code

import {
  Injectable,
  OnModuleInit,
  OnModuleDestroy,
  Logger,
} from "@nestjs/common";

import {
  Kafka,
  Producer,
  Consumer,
  Admin,
  EachMessagePayload,
  CompressionTypes,
} from "kafkajs";

export interface KafkaMessage<T = unknown> {
  key?: string;
  value: T;
  headers?: Record<string, string>;
}

@Injectable()
export class KafkaService implements OnModuleInit, OnModuleDestroy {
  private readonly logger = new Logger(KafkaService.name);
  private kafka: Kafka;
  private producer: Producer;
  private consumers: Map<string, Consumer> = new Map();
  private admin: Admin;

  async onModuleInit() {
    this.kafka = new Kafka({
      clientId: process.env.KAFKA_CLIENT_ID || "nestjs-app",
      brokers: (process.env.KAFKA_BROKERS || "localhost:9092").split(","),
      ssl: process.env.KAFKA_SSL === "true",
      sasl:
        process.env.KAFKA_USERNAME && process.env.KAFKA_PASSWORD
          ? {
              mechanism: "plain",
              username: process.env.KAFKA_USERNAME,
              password: process.env.KAFKA_PASSWORD,
            }
          : undefined,
      retry: {
        initialRetryTime: 100,
        retries: 8,
      },
    });

    this.producer = this.kafka.producer({
      allowAutoTopicCreation: true,
      transactionTimeout: 30000,
    });

    this.admin = this.kafka.admin();

    await this.producer.connect();
    await this.admin.connect();
    this.logger.log("Kafka producer and admin connected");
  }

  async onModuleDestroy() {
    await this.producer.disconnect();
    await this.admin.disconnect();
    
    for (const [groupId, consumer] of this.consumers) {
      await consumer.disconnect();
      this.logger.log(`Consumer ${groupId} disconnected`);
    }
    
    this.logger.log("Kafka disconnected");
  }

  async isHealthy(): Promise<boolean> {
    try {
      const topics = await this.admin.listTopics();
      return Array.isArray(topics);
    } catch {
      return false;
    }
  }

  // Producer methods
  async publish<T>(
    topic: string,
    message: KafkaMessage<T>
  ): Promise<void> {
    await this.producer.send({
      topic,
      compression: CompressionTypes.GZIP,
      messages: [
        {
          key: message.key,
          value: JSON.stringify(message.value),
          headers: message.headers,
        },
      ],
    });
  }

  async publishBatch<T>(
    topic: string,
    messages: KafkaMessage<T>[]
  ): Promise<void> {
    await this.producer.send({
      topic,
      compression: CompressionTypes.GZIP,
      messages: messages.map((msg) => ({
        key: msg.key,
        value: JSON.stringify(msg.value),
        headers: msg.headers,
      })),
    });
  }

  // Consumer methods
  async subscribe<T>(
    topic: string,
    groupId: string,
    handler: (message: T, metadata: EachMessagePayload) => Promise<void>
  ): Promise<void> {
    const consumer = this.kafka.consumer({ groupId });
    await consumer.connect();
    await consumer.subscribe({ topic, fromBeginning: false });

    await consumer.run({
      eachMessage: async (payload) => {
        const { message } = payload;
        if (message.value) {
          const parsed = JSON.parse(message.value.toString()) as T;
          await handler(parsed, payload);
        }
      },
    });

    this.consumers.set(groupId, consumer);
    this.logger.log(`Subscribed to ${topic} with group ${groupId}`);
  }

  async unsubscribe(groupId: string): Promise<void> {
    const consumer = this.consumers.get(groupId);
    if (consumer) {
      await consumer.disconnect();
      this.consumers.delete(groupId);
      this.logger.log(`Unsubscribed consumer ${groupId}`);
    }
  }

  // Admin methods
  async createTopic(
    topic: string,
    numPartitions = 1,
    replicationFactor = 1
  ): Promise<void> {
    await this.admin.createTopics({
      topics: [
        {
          topic,
          numPartitions,
          replicationFactor,
        },
      ],
    });
    this.logger.log(`Created topic: ${topic}`);
  }

  async deleteTopic(topic: string): Promise<void> {
    await this.admin.deleteTopics({ topics: [topic] });
    this.logger.log(`Deleted topic: ${topic}`);
  }

  async listTopics(): Promise<string[]> {
    return this.admin.listTopics();
  }
}

Usage

await this.kafka.publish('topic', { key: 'id', value: data });

Dependencies

Frequently Asked Questions

How do I install Kafka Service?
Run the following command: npx hanma add kafka-service --framework nest
What dependencies are required?
This snippet requires: kafkajs. Make sure to install them before using the snippet.
How do I use this snippet?
Copy the code and paste it into your project. The snippet will be saved to src/providers/kafka.service.ts.
Can I customize this snippet?
Yes! All snippets are copy-paste code that you own. Feel free to modify them to fit your needs.
Does this work with other frameworks?
This snippet is designed for the specific framework. Check the other framework pages for equivalent implementations.