npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

bunny-route

v0.2.0

Published

Express-inspired, high-performance RabbitMQ client for Node.js

Downloads

216

Readme

🐇 Bunny Route

Bunny Route is a high-performance, Express-inspired RabbitMQ client for Node.js, designed to streamline and standardize RabbitMQ integration out of the box.

✨ Features

  • Express-like architecture for familiar and intuitive usage
  • Powerful middleware system for pre- and post-processing of messages
  • Simplified RabbitMQ setup with automatic queue and exchange creation
  • Automatic connection management and recovery
  • Built-in retry mechanism for failed message processing
  • Support for direct messaging patterns
  • RPC (Remote Procedure Call) support for request-response scenarios
  • Dead-letter queue support for unprocessable messages
  • Support for both CommonJS and ES modules
  • Flexibility to use custom exchanges or default exchanges based on appName
  • Compatibility with Nest.js microservices

📦 Installation

npm install bunny-route

🚀 Usage Examples

Server (Consumer) Examples

Default Exchange (based on appName)

const { RMQServer } = require('bunny-route');

const server = new RMQServer({
  uri: 'amqp://localhost',
  appName: 'my-service',
  retryOptions: {
    maxRetries: 3,
    retryTTL: 5000,
    enabled: true
  }
});

server.on('user.created', async (data, ctx) => {
  console.log('New user created:', data);
  // Process the message
});

server.listen().then(() => {
  console.log('Bunny Route server is now listening for messages');
});

Custom Exchange

const { RMQServer } = require('bunny-route');

const server = new RMQServer({
  uri: 'amqp://localhost',
  appName: 'my-service',
  exchange: 'custom-exchange',
  retryOptions: {
    maxRetries: 3,
    retryTTL: 5000,
    enabled: true
  }
});

server.on('user.created', async (data, ctx) => {
  console.log('New user created:', data);
  // Process the message
});

server.listen().then(() => {
  console.log('Bunny Route server is now listening for messages');
});

Integration with Existing Backend

const { RMQClient } = require('bunny-route');

const client = new RMQClient({
  uri: 'amqp://localhost',
  appName: 'my-service',
  exchange: 'existing-exchange'
});

async function publishMessage() {
  await client.connect();
  await client.send('user.created', { id: 123, name: 'John Doe' });
  await client.close();
}

publishMessage().catch(console.error);

Client (Publisher) Examples

Default Exchange (based on appName)

const { RMQClient } = require('bunny-route');

const client = new RMQClient({
  uri: 'amqp://localhost',
  appName: 'my-service'
});

async function publishMessage() {
  await client.connect();
  await client.send('user.created', { id: 123, name: 'John Doe' });
  await client.close();
}

publishMessage().catch(console.error);

Custom Exchange

const { RMQClient } = require('bunny-route');

const client = new RMQClient({
  uri: 'amqp://localhost',
  appName: 'my-service',
  exchange: 'custom-exchange'
});

async function publishMessage() {
  await client.connect();
  await client.send('user.created', { id: 123, name: 'John Doe' });
  await client.close();
}

publishMessage().catch(console.error);

🔑 Key Concepts

  • appName: Used as a prefix for queue names and as the default exchange name if exchange is not provided.
  • exchange: The exchange to use for publishing and consuming messages. If not provided, the appName will be used as the exchange name.
  • Queues: The main queue is named ${appName}, with additional queues for retries and dead-letters.
  • Routing: Messages are routed based on the routing keys defined in on handlers.

🔄 RPC Mechanism

Bunny Route supports RPC-style communication:

  • The server can return values from its handlers, which will be sent back to the client for RPC calls.
  • The client can make RPC calls using the send method with a timeout option, which will wait for a response.

🚨 Error Handling and Retries

  • Failed message processing triggers automatic retries based on the configured retryOptions.
  • Messages exceeding the maximum retry count are sent to a dead-letter queue (${appName}.dlq).
  • Connection errors are automatically handled with reconnection attempts.

🎭 Middleware System

One of Bunny Route's most powerful features is its Express-inspired middleware system. The middleware pipeline allows you to create reusable processing chains for your messages, perfect for cross-cutting concerns like logging, validation, and error handling.

How Middleware Works

Middleware functions in Bunny Route follow this signature:

type MiddlewareFunction = (
  context: HandlerContext,
  next: () => Promise<void>,
  reply: ReplyFunction
) => Promise<void>;

Where:

  • context: Contains message details including content, routing key, and headers
  • next: Function to call the next middleware in the chain
  • reply: Function to send a response back to the client

Middleware Features

  • Sequential Processing: Middlewares are executed in the order they are added
  • Flow Control: Each middleware can decide whether to continue the chain by calling next()
  • Early Response: Middleware can send responses using reply() and prevent further processing
  • Error Handling: Catch and handle errors from subsequent middleware or message handlers
  • Context Modification: Add or modify data in the context for use in later middleware or handlers
  • Headers Access: Full access to message headers for metadata handling
  • Reply Control: Send multiple replies or format responses before they reach the client

Middleware Examples

Basic Logging Middleware

server.use(async (context, next, reply) => {
  console.log(`[${new Date().toISOString()}] Incoming message:`, {
    routingKey: context.routingKey,
    content: context.content
  });
  
  await next();
  
  console.log(`[${new Date().toISOString()}] Processing completed`);
});

Example Authentication Middleware

// Note: This is an example using a custom x-api-key header.
// In a real RabbitMQ setup, you'd typically use built-in authentication mechanisms
server.use(async (context, next, reply) => {
  if (!context.headers['x-api-key']) {
    reply({ error: 'Unauthorized: API key missing' });
    return; // Stop middleware chain
  }
  
  // Authenticate API key (example implementation)
  if (!isValidApiKey(context.headers['x-api-key'])) {
    reply({ error: 'Unauthorized: Invalid API key' });
    return;
  }
  
  await next();
});

Request Timing Middleware

server.use(async (context, next, reply) => {
  const label = `message-${context.routingKey}`;
  console.time(label);
  
  await next();
  
  console.timeEnd(label);
});

Error Handling Middleware

server.use(async (context, next, reply) => {
  try {
    await next();
  } catch (error) {
    console.error('Error processing message:', error);
    reply({ 
      error: error.message,
      status: 'error',
      timestamp: new Date().toISOString()
    });
  }
});

Best Practices

  • Order Matters: Place middleware in logical order (e.g., authentication before validation)
  • Don't Forget next(): Always call next() unless you intentionally want to stop the chain
  • Error Handling: Use try-catch blocks in middleware to handle errors gracefully
  • Keep it Focused: Each middleware should have a single responsibility
  • Performance Aware: Be mindful of async operations and their impact on message processing

Nest.js Compatibility

Bunny Route offers support for Nest.js microservices using RabbitMQ transport. This feature aligns with Nest.js's default RabbitMQ configuration.

Note: The Nest.js approach to RabbitMQ might seem counterintuitive to those familiar with RabbitMQ best practices. Instead of using exchanges and routing keys for message distribution (content-based routing), Nest.js uses a single queue and relies on an internal routing mechanism based on the pattern field in the message. While this deviates from traditional RabbitMQ usage patterns and may not fully utilize RabbitMQ's capabilities, it's the standard approach in the Nest.js ecosystem that we need to accommodate for compatibility.

We will update this documentation if we discover that Nest.js supports more advanced RabbitMQ features like custom bindings.

Nest.js Microservice Configuration

First, let's look at how to set up a standard Nest.js microservice using RabbitMQ:

// main.ts
import { NestFactory } from '@nestjs/core';
import { Transport, MicroserviceOptions } from '@nestjs/microservices';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.createMicroservice<MicroserviceOptions>(AppModule, {
    transport: Transport.RMQ,
    options: {
      urls: ['amqp://localhost:5672'],
      queue: 'user-service',
      queueOptions: {
        durable: false
      },
    },
  });
  await app.listen();
}
bootstrap();

Nest.js Controller with @MessagePattern

Now, let's look at a Nest.js controller that uses @MessagePattern to handle incoming messages:

// user.controller.ts
import { Controller } from '@nestjs/common';
import { MessagePattern } from '@nestjs/microservices';

@Controller()
export class UserController {
  @MessagePattern('user.create')
  async createUser(data: { name: string, email: string }) {
    console.log('Creating user:', data);
    // Create user logic here
    return { status: 'User created successfully' };
  }

  @MessagePattern('user.get')
  async getUser(data: { id: number }) {
    console.log('Fetching user:', data);
    // Get user logic here
    return { id: data.id, name: 'John Doe', email: '[email protected]' };
  }
}

Bunny Route Client for Nest.js

To interact with this Nest.js microservice using Bunny Route, configure the RMQClient as follows:

const { RMQClient } = require('bunny-route');

const client = new RMQClient({
  uri: 'amqp://localhost',
  appName: 'user-service', // This MUST match the queue name in Nest.js configuration
  exchange: '' // Nest.js uses the default exchange
});

async function interactWithNestService() {
  await client.connect();

  // Create a user
  const createResponse = await client.send(
    'user-service',
    { 
      pattern: 'user.create',
      data: { name: 'John Doe', email: '[email protected]' }
    }, 
    { nestCompatible: true }
  );
  console.log('Create User Response:', createResponse);

  // Get a user
  const getResponse = await client.send(
    'user-service',
    { 
      pattern: 'user.get',
      data: { id: 1 }
    }, 
    { nestCompatible: true }
  );
  console.log('Get User Response:', getResponse);

  await client.close();
}

interactWithNestService().catch(console.error);

Note:

  • The nestCompatible option is set to false by default.
  • When using nestCompatible: true, the send method expects:
    • The first parameter to be the queue name (matching the appName in RMQClient and the queue in Nest.js microservice configuration)
    • The second parameter to be an object with pattern (matching @MessagePattern in Nest.js) and data
  • All your data should be nested within the data field of your message to ensure proper handling by Nest.js.
  • The pattern in your client's send method should match the @MessagePattern decorator in your Nest.js controller.
  • Nest.js uses a single queue for the entire microservice, specified in the configuration.
  • The routing to specific handlers is done internally by Nest.js based on the pattern field.
  • This implementation follows Nest.js's default RabbitMQ configuration. If you've customized your Nest.js RabbitMQ setup, you may need to adjust accordingly.

📚 Documentation

For more detailed documentation, please refer to the Wiki.

🤝 Contributing

We welcome contributions! Please see our Contributing Guide for more details.

📄 License

Bunny Route is MIT licensed.