DEV Community

Cover image for Migrating a LavinMQ Node.js app from `amqplib` to `amqp-client.js`

Migrating a LavinMQ Node.js app from `amqplib` to `amqp-client.js`

If you have an existing Node.js application using amqplib, changing AMQP clients might sound like rewriting a large part of your application.

I wanted to see how much actually had to change.

I built a small banking application that publishes transactions to LavinMQ, processes them in a worker, retries temporary failures, dead-letters permanent failures, and protects against duplicate processing.

The application separates the banking logic from the client-specific messaging code through a broker.js layer.

I first implemented that layer with amqplib, then migrated it to amqp-client.js, an AMQP client developed by CloudAMQP.

The interesting result was that most of the application didn't need to change.

What stayed the same?

The migration didn't require any application redesign.

The following parts stayed the same:

  • Banking logic
  • Queue and exchange topology
  • Retry strategy
  • Dead-letter handling
  • Idempotency logic

The worker still makes the same application-level decisions: process the transaction, retry a temporary failure, dead-letter a permanent failure, or acknowledge a transaction that has already been processed.

The client-specific messaging operations are kept behind broker.js.

Conceptually, the application looks like this:

Application logic
       |
       v
   broker.js
       |
       v
  AMQP client
       |
       v
    LavinMQ
Enter fullscreen mode Exit fullscreen mode

This separation became particularly useful when I started replacing one AMQP client with the other.

What actually changed?

Most of the changes were isolated to broker.js.

That's where the differences between the two clients became much clearer:

  • Establishing connections
  • Publishing messages
  • Consuming messages
  • Acknowledging messages
  • Managing connection recovery

For example, the worker can interact with the broker through the same small interface:

await broker.connect();
await broker.subscribe(handleMessage);
Enter fullscreen mode Exit fullscreen mode

When processing a transaction, it can also use operations such as:

await broker.retry(
  transaction,
  retries + 1
);

await broker.sendToDeadQueue(
  transaction,
  error,
  retries
);

broker.acknowledge(message);
Enter fullscreen mode Exit fullscreen mode

The worker decides what should happen to a transaction.

The broker layer handles how the corresponding messaging operation is performed using the AMQP client.

That meant I could change the implementation behind broker.js without redesigning the banking application's reliability logic.

Connection recovery was the biggest difference

Connection recovery was where I noticed the biggest difference between the two approaches.

With amqplib, I handled recovery in the broker layer.

When the connection is lost, the application needs to detect the closed connection, reconnect, create a new channel, and restore the consumer.

Conceptually:

Connection lost
      |
      v
Detect connection close
      |
      v
Wait and reconnect
      |
      v
Create a new channel
      |
      v
Restore the consumer
      |
      v
Continue processing
Enter fullscreen mode Exit fullscreen mode

This works, but the application owns the recovery behavior.

With the high-level AMQPSession API in amqp-client.js, more of that connection lifecycle is handled by the client.

The session can reconnect when the connection is interrupted and restore its subscriptions after the connection is recovered.

Conceptually:

Connection lost
      |
      v
AMQPSession detects it
      |
      v
Reconnect
      |
      v
Restore subscription
      |
      v
Continue processing
Enter fullscreen mode Exit fullscreen mode

The important difference isn't that one client can recover and the other cannot.

Both can be used to build an application that recovers from connection failures.

The difference is where the responsibility for recovery lives.

With amqplib, I implemented it in the broker layer.

With the high-level AMQPSession API, connection and subscription recovery are handled by the client.

Reliability didn't move into the client

One thing I found particularly interesting was that changing clients didn't change most of the reliability patterns in the application.

Retries were still retries.

Dead-lettering was still dead-lettering.

Messages still needed acknowledgements.

Queues still needed the appropriate durability settings.

Transactions still needed to be processed idempotently to protect against duplicate delivery.

Those concerns weren't suddenly solved by changing the AMQP client.

The major difference I observed was how much connection lifecycle management the application had to handle itself.

The main takeaway

The migration reinforced something I hadn't fully appreciated when I started the experiment:

Changing an AMQP client doesn't necessarily mean changing your application's reliability design.

By keeping client-specific messaging code behind a broker layer, I could migrate from amqplib to amqp-client.js without rewriting the banking logic.

The business behavior remained the same.

The messaging architecture remained the same.

The implementation behind broker.js changed.

I wrote a full step-by-step migration guide covering the changes to connections, publishing, consuming, acknowledgements, and connection recovery:

👉 Read the full migration guide on CloudAMQP

The complete banking demo contains both implementations if you want to compare the code or run the experiment yourself:

👉 View the Node.js clients demo on GitHub

I'd be interested to hear how others approach this.

Where does connection recovery live in your Node.js messaging applications — in your application code, a wrapper around the client, or the client library itself?

And if you've migrated between AMQP clients before, what part of the migration caused the most work?

Feel free to leave questions about the implementation too.

Top comments (0)