Configure channels
Channel options live on the low-level AmqpClient from @amqp-contract/core. TypedAmqpClient and TypedAmqpWorker cover almost everything, so reach for this only when you need channel behaviour they do not expose.
Know the defaults
AmqpClient creates confirm channels and encodes payloads itself — JSON for values, Buffers passed through untouched. amqp-connection-manager's own json mode is always forced off, because it would stringify a Buffer and corrupt compressed messages:
import { AmqpClient } from "@amqp-contract/core";
const client = new AmqpClient(contract, {
urls: ["amqp://localhost"],
});Publisher confirms are on because without them publish cannot tell you whether the broker accepted the message — it would report success as soon as the bytes were written to the socket.
A raw AmqpClient declares every resource of the contract it is given, on every (re)connect, according to its topology option: "assert" (default) declares, "passive" only checks existence, "none" touches nothing. TypedAmqpClient and TypedAmqpWorker hand it only their role's slice of the contract. It connects through exactly one of urls (a pooled connection, partitioned by connectionPool — "default" unless set; the typed client uses "client", the worker "worker") or connection (an AmqpConnectionManager you own, never closed by the client). isConnected() reports whether that connection is up.
Customize a channel
import type { Channel } from "amqplib";
const client = new AmqpClient(contract, {
urls: ["amqp://localhost"],
channelOptions: {
confirm: true,
name: "orders-publisher",
setup: async (channel: Channel) => {
await channel.prefetch(10);
},
},
});name appears in RabbitMQ's management UI and in logs — worth setting when a process opens several channels, because otherwise they are indistinguishable when you go looking for the one that is misbehaving.
Add resources the contract does not describe
The setup function runs after the contract's topology is established — the client's or worker's slice of it, or all of it on a raw AmqpClient — so those resources already exist when it fires:
channelOptions: {
setup: async (channel: Channel) => {
await channel.prefetch(10);
await channel.assertQueue("legacy-integration-queue");
await channel.assertExchange("legacy-exchange", "topic");
},
},This is the escape hatch for integrating with topology you do not own — a legacy queue another team declares, for instance. Anything you can express in the contract belongs in the contract, where it is typed and documented.
Keep setup idempotent
setup runs on every reconnection, not once at startup. Anything with side effects beyond declaring topology will run repeatedly:
setup: async (channel: Channel) => {
await channel.prefetch(10); // fine — idempotent
await channel.assertQueue("queue"); // fine — idempotent
await incrementDeploymentCounter(); // wrong — fires on every reconnect
},Declaring exchanges and queues is idempotent by design, so topology work is safe. Bookkeeping is not.
Use the callback form
A callback-style setup is supported for compatibility:
setup: (channel: Channel, callback: (error?: Error) => void) => {
channel
.prefetch(10)
.then(() => callback())
.catch((err) => callback(err));
},Prefer the promise form.
Understand what you cannot do
setup cannot suppress contract topology. It runs after the client's or worker's slice of the contract is declared, so those exchanges, queues and bindings already exist. To skip declaring them — topology owned by IaC, or credentials without configure permission — pass topology: "none" (or "passive", to only check they exist) to create() instead.
Channel options are per channel. Connection-level settings are separate:
const client = new AmqpClient(contract, {
urls: ["amqp://localhost"],
connectionOptions: { heartbeatIntervalInSeconds: 30 }, // connection
channelOptions: { confirm: false }, // channel
});Note that connectionOptions participates in the connection cache key — see share connections.
Diagnose setup that seems not to run
Nothing happens. Confirm the channel is actually being created — channels are created lazily, so a client that never publishes never runs setup.
Resources are missing after a reconnect. Something in setup threw. A rejected setup aborts the rest of it, so a failure on the first line silently loses everything after. Log inside it.
prefetch appears to have no effect. Setting prefetch here applies to the channel, but the worker sets its own per-consumer prefetch, which wins. Use defaultConsumerOptions or the per-handler form instead — see consume messages.
Where next
- Consume messages — the supported way to set prefetch.
- Share connections — connection-level options.
- Tune performance — publisher confirms and throughput.