An interactive Next.js application for learning RabbitMQ exchange types. Each exchange type has a dedicated tutorial page with explanations, a code structure overview, and a live demo you can run against a real RabbitMQ instance.
docker run -d \
--name rabbitmq \
-p 5672:5672 \
-p 15672:15672 \
rabbitmq:managementdocker run -d \
--name rabbitmq \
-p 5672:5672 \
-p 15672:15672 \
-v rabbitmq_data:/var/lib/rabbitmq \
--hostname rabbitmq-host \
rabbitmq:managementCreate a docker-compose.yml file:
version: "3.8"
services:
rabbitmq:
image: rabbitmq:management
container_name: rabbitmq
ports:
- "5672:5672" # AMQP protocol port
- "15672:15672" # Management UI port
volumes:
- rabbitmq_data:/var/lib/rabbitmq
environment:
RABBITMQ_DEFAULT_USER: guest
RABBITMQ_DEFAULT_PASS: guest
volumes:
rabbitmq_data:Then run:
docker compose up -dOpen the Management UI: http://localhost:15672
- Username:
guest - Password:
guest
You should see the RabbitMQ dashboard. The AMQP port 5672 is used by the application.
# Check if RabbitMQ is running
docker ps | grep rabbitmq
# View logs
docker logs rabbitmq
# Stop RabbitMQ
docker stop rabbitmq
# Start again
docker start rabbitmq
# Remove container (data lost unless using volume)
docker rm -f rabbitmq# Install dependencies
npm install
# Start the development server
npm run devOpen http://localhost:3000 to see the tutorial home page.
The home page shows a live connection status indicator — green means RabbitMQ is reachable.
By default the app connects to amqp://guest:guest@localhost:5672.
To use a different RabbitMQ instance, create a .env.local file:
RABBITMQ_URL=amqp://username:password@your-host:5672Each exchange type follows a strict Setup → Publish → Consume pattern. The responsibilities are split across three separate API routes:
| Route | Responsibility |
|---|---|
setup/route.ts |
Creates the exchange, queues, and bindings. Must be called first. |
publish/route.ts |
Publishes a message to the exchange. Assumes setup has been done. |
consume/route.ts |
Reads messages from a queue. Assumes setup has been done. |
Why? In RabbitMQ, messages are routed to queues at the moment of publishing. If a queue doesn't exist yet (or isn't bound to the exchange), the message is silently dropped. The setup step ensures all queues and bindings exist before any messages are sent.
Follow the tutorials in order for the best learning experience. On each page, always click Setup first before publishing.
A fanout exchange broadcasts every message to all bound queues. The routing key is ignored.
Try it:
- Open http://localhost:3000/tutorial/fanout
- Click Setup Exchange & Queues
- Click Publish to send a message
- Consume from
fanout.queue.1— you get the message - Consume from
fanout.queue.2— you get the same message again!
Key concept: One publish → all queues receive a copy.
A direct exchange routes messages to queues whose binding key exactly matches the routing key.
Try it:
- Open http://localhost:3000/tutorial/direct
- Click Setup Exchange & Queues
- Publish with routing key
error - Consume with routing key
error→ you get the message ✓ - Consume with routing key
info→ you get nothing ✗
Key concept: Exact match routing — like a postal address.
A topic exchange routes using wildcard pattern matching on dot-separated routing keys.
*matches exactly one word#matches zero or more words
Try it:
- Open http://localhost:3000/tutorial/topic
- Click Setup Exchange & Queues
- Publish with key
logs.error - Publish with key
logs.error.critical - Consume with pattern
logs.*→ gets onlylogs.error(one word after logs) - Consume with pattern
logs.#→ gets both messages (any words after logs)
Key concept: Flexible pattern-based routing.
A headers exchange routes based on message header attributes. The routing key is ignored.
x-match: all— ALL specified headers must match (AND logic)x-match: any— ANY specified header must match (OR logic)
Try it:
- Open http://localhost:3000/tutorial/headers
- Click Setup Exchange & Queues
- Publish with headers
{"format":"pdf","type":"report"} - Consume with
x-match=alland headers{"format":"pdf","type":"report"}→ match ✓ - Consume with
x-match=alland headers{"format":"pdf","type":"invoice"}→ no match ✗ - Consume with
x-match=anyand headers{"format":"pdf","type":"invoice"}→ match ✓ (format matches)
Key concept: Attribute-based routing without routing keys.
A Dead Letter Queue (DLQ) captures messages that cannot be processed.
Messages become "dead letters" when:
- A consumer rejects them with
nack(msg, false, false)(requeue=false) - They expire (TTL elapsed)
- The queue exceeds its length limit
Try it (rejection):
- Open http://localhost:3000/tutorial/deadletter
- Click Setup Exchanges & Queues
- Click Publish to Main Queue
- Click Reject Message (NACK) — simulates a failed consumer
- Click Consume from DLQ — see the rejected message with
x-deathmetadata
Try it (TTL expiry):
- After setup, publish with TTL = 3000ms
- Wait 3 seconds (don't reject)
- Click Consume from DLQ — message expired and moved automatically
Key concept: Never lose a message — capture failures in a DLQ for inspection and retry.
app/
├── page.tsx # Tutorial home page
├── api/
│ ├── status/route.ts # GET: check RabbitMQ connection
│ ├── fanout/
│ │ ├── setup/route.ts # POST: create exchange, queues, bindings
│ │ ├── publish/route.ts # POST: publish to fanout exchange
│ │ └── consume/route.ts # GET: consume from a named queue
│ ├── direct/
│ │ ├── setup/route.ts # POST: create exchange, queues, bindings
│ │ ├── publish/route.ts # POST: publish with routing key
│ │ └── consume/route.ts # GET: consume by routing key
│ ├── topic/
│ │ ├── setup/route.ts # POST: create exchange, queues, bindings
│ │ ├── publish/route.ts # POST: publish with dot-separated key
│ │ └── consume/route.ts # GET: consume with wildcard pattern
│ ├── headers/
│ │ ├── setup/route.ts # POST: create exchange, queues, bindings
│ │ ├── publish/route.ts # POST: publish with custom headers
│ │ └── consume/route.ts # GET: consume with x-match filter
│ └── deadletter/
│ ├── setup/route.ts # POST: create both exchanges, both queues, bindings
│ ├── publish/route.ts # POST: publish to main queue (optional TTL)
│ ├── reject/route.ts # POST: NACK a message → routes to DLX
│ └── consume/route.ts # GET: consume from dead letter queue
└── tutorial/
├── fanout/page.tsx # Fanout tutorial page
├── direct/page.tsx # Direct tutorial page
├── topic/page.tsx # Topic tutorial page
├── headers/page.tsx # Headers tutorial page
└── deadletter/page.tsx # Dead letter queue tutorial page
lib/
└── rabbitmq.ts # Shared: createConnection / closeConnection
| Type | Routing Logic | Routing Key? | Best For |
|---|---|---|---|
| Fanout | All bound queues | ❌ Ignored | Broadcasting, notifications |
| Direct | Exact key match | ✅ Exact match | Log levels, task routing |
| Topic | Pattern matching | ✅ Wildcards (* #) | Flexible routing, microservices |
| Headers | Header attributes | ❌ Ignored | Attribute-based routing |
Publisher → Exchange → Queue → Consumer
- Publisher sends messages to an exchange (never directly to a queue)
- Exchange routes messages to queues based on type and binding rules
- Queue stores messages until a consumer retrieves them
- Consumer receives messages; load is balanced across multiple consumers
- AMQP (used by this tutorial) — port 5672
- STOMP, MQTT, RabbitMQ Streams
- durable — survives broker restart
- auto-delete — deleted when last consumer disconnects
- exclusive — only accessible by the declaring connection
When a message is dead-lettered, RabbitMQ adds an x-death header containing:
queue— original queue namereason—rejected,expired, ormaxlentime— when it was dead-letteredexchange— original exchangerouting-keys— original routing keys
"RabbitMQ Not Connected" on the home page
- Make sure Docker is running:
docker ps | grep rabbitmq - Start RabbitMQ:
docker start rabbitmq - Check logs:
docker logs rabbitmq
Port already in use
# Check what's using port 5672
lsof -i :5672
# Or use different ports
docker run -d --name rabbitmq -p 5673:5672 -p 15673:15672 rabbitmq:management
# Then set RABBITMQ_URL=amqp://guest:guest@localhost:5673 in .env.localQueue already exists with different parameters
- Go to the Management UI (http://localhost:15672)
- Navigate to Queues → delete the conflicting queue
- Or restart RabbitMQ:
docker restart rabbitmq
Messages not appearing after publish
- Make sure you clicked Setup before publishing. If the queues didn't exist at publish time, the messages were silently dropped by RabbitMQ.
- Click Setup again (it's idempotent), then publish again.