# @imqueue vs NestJS microservices: framework vs transport

Source: https://imqueue.org/blog/imqueue-vs-nestjs/
Published: 2026-06-17
Updated: 2026-07-29
Author: Andrii Glushko — Maintainer, @imqueue (https://github.com/creomobile)

**NestJS and `@imqueue` sit at different layers, so "which is better" is the wrong question.** NestJS is a full application framework whose microservices module can send messages over a pluggable transporter. `@imqueue` is a focused RPC layer: self-describing service classes over a message queue, with typed clients generated from them. Understanding the layering is more useful than declaring a winner — and in a lot of systems the answer is both.

(NestJS details reflect its documented behavior at the time of writing.)

## Different layers of the stack

**NestJS** is a full application framework: dependency injection, modules, controllers, guards, interceptors, an opinionated structure for the whole app. Its microservices module lets a Nest app send and receive messages over a transporter — Redis, NATS, Kafka, RabbitMQ, MQTT, gRPC, TCP — using decorators like `@MessagePattern()` to bind handlers to message patterns.

**@imqueue** is not an application framework. It's a focused RPC layer: service classes extending `IMQService`, with typed clients generated from the services. It has no opinions about DI, HTTP controllers, or app structure — it's the piece that moves typed calls between services.

So the honest framing is: NestJS is *how you might build a whole service*; `@imqueue` is *how services call each other*.

## The NestJS vs `@imqueue` typing difference, in code

Typing across the call boundary is the substantive difference between the two, and it's easier to see than to describe.

With the NestJS microservices module, a caller uses a `ClientProxy` and sends to a message pattern. The pattern is a string or object, and the payload and response types are generics you supply:

```typescript
// handler side
@MessagePattern({ cmd: 'user.get' })
getUser(id: string) {
  return this.users.findById(id);
}

// caller side — the types are an assertion, not a derivation
const user = await firstValueFrom(
  this.client.send<User, string>({ cmd: 'user.get' }, '42'),
);
```

That's clean and decorator-driven, but nothing checks that `{ cmd: 'user.get' }` still exists, or that `User` still matches what the handler returns. The pattern is a runtime string; the types are a promise you make to the compiler on both sides independently.

With `@imqueue`, the client is generated *from* the service, so there's no promise to keep in sync:

```typescript
import { IMQService, expose } from '@imqueue/rpc';

export class User extends IMQService {
    /**
     * Returns a user by id
     *
     * @param {string} id - user identifier
     * @return {Promise<{ id: string; name: string } | null>}
     */
    @expose()
    public async get(id: string): Promise<{ id: string; name: string } | null> {
        return { id, name: 'Jane Doe' };
    }
}
```

```bash
imq client generate User ./src/clients
```

```typescript
import { user } from './clients/index.js';

const client = new user.UserClient({ callTimeout: 5000 });
await client.start();

const found = await client.get('42'); // signature came from the service
```

Change `get`'s signature, regenerate, and every mismatched call site becomes a compile error. There's no pattern string to typo and no response generic to get wrong — but note the JSDoc block isn't decoration: it's the *only* type source the generator reads, so an unannotated parameter silently becomes `any`.

## Addressing and routing

A second real difference, easy to miss.

NestJS message patterns are an application-level routing key you design: `{ cmd: 'user.get' }`, `'user.created'`, whatever convention you settle on. You own the namespace, and you own keeping it coherent as the system grows.

`@imqueue` has no routing keys, because the **queue name is the address** and it's the service class name. There's no service discovery to configure and no load balancer to wire up — instances of a service compete for messages on its queue, so work distributes across replicas by consumption. That's a smaller surface to design, and a smaller surface to get wrong; it's also less flexible if you *want* content-based routing, which `@imqueue` doesn't do.

If that model is new to you, [do Node.js backends even need service discovery?](https://imqueue.org/blog/do-nodejs-backends-need-service-discovery/) and [load balancing without a load balancer](https://imqueue.org/blog/load-balancing-microservices-without-a-load-balancer/) cover the consequences.

## Transport choice

NestJS's pluggable transporters are a genuine advantage worth stating plainly. If you need Kafka because the rest of your organisation is on Kafka, or NATS because you want its semantics, NestJS gets you there with a configuration change and the same `@MessagePattern()` handlers.

`@imqueue` ships one transport: Redis. `vendor` defaults to `'Redis'` and is the only supported value, though the `IMessageQueue` interface is the documented seam if you ever implement another. If your transport is already decided and it isn't Redis, that decides this comparison on its own.

## Feature scope and structure

NestJS gives you a great deal of structure and a huge ecosystem — validation pipes, config, testing utilities, an enormous plugin catalog. If you want a framework to build entire services in, that's its job, and `@imqueue` doesn't compete with it.

`@imqueue` keeps its footprint small and pairs with [`@imqueue/cli`](https://imqueue.org/cli/) for scaffolding services, wiring VCS/CI/registry providers, generating clients, and running a local fleet. You bring your own app structure.

## Where NestJS is the better choice

- **You want one framework to build whole services in**, with DI, guards, interceptors and a plugin for most things.
- **Your transport isn't Redis** — Kafka, NATS, RabbitMQ and friends are first-class there and absent here.
- **You need content-based routing** or event patterns richer than "one queue per service".
- **You're already on Nest.** The microservices module is a small addition to a codebase that already has the framework's shape.
- **You want a large hiring pool and a big ecosystem.** Nest is far more widely known.

## Where @imqueue's model costs you

- **Redis only**, as above.
- **JSDoc is mandatory** for exposed methods, and `removeComments: false` is required in consuming projects or the generator sees nothing.
- **Client generation needs the service running**, which is a real step in CI and in a fresh checkout.
- **`callTimeout` is unset by default** — a call to a service that never answers stays pending forever. Set it.
- **Delivery is at-least-once** in both modes, so exposed methods should be idempotent. Safe delivery re-queues a message a dying worker never *started*; it doesn't protect work already in flight, and draining is on you. [Graceful shutdown and zero-drop deploys](https://imqueue.org/blog/graceful-shutdown-zero-drop-deploys/) works through what that actually takes.
- **No streaming**, and no built-in circuit breakers, gateway or metrics breadth — the core is deliberately narrow.

## Can you use them together?

Largely, yes — they're not mutually exclusive, and this is a reasonable architecture:

```
HTTP ──▶ NestJS edge app ──@imqueue──▶ user, billing, search…
```

Build a service's internals however you like — including with Nest — and use `@imqueue` as the typed transport between backend services, while a NestJS app handles the HTTP edge. Nest's DI doesn't object to a generated client being constructed in a provider; it's a normal class.

The main thing to decide deliberately is which tool *owns* service-to-service calls, so you aren't running two RPC mechanisms over the same traffic. Running Nest's Redis transporter *and* `@imqueue` side by side over the same Redis is the configuration most likely to confuse everyone six months later.

## Quick comparison

| | @imqueue | NestJS (microservices) |
|---|---|---|
| Layer | RPC transport + typed clients | Full application framework |
| Transport | Message queue (Redis only) | Pluggable (Redis, NATS, Kafka, RabbitMQ, gRPC, …) |
| Addressing | Queue name = service class name | Message patterns you design |
| Inter-service typing | Client generated from the service | Generics you assert on both sides |
| Contract drift caught | At regeneration, as compile errors | Not caught by the compiler |
| App structure | Unopinionated | Opinionated (DI, modules, controllers) |
| Service discovery | None needed | None needed (broker-dependent) |
| Scope | Small, focused | Broad ecosystem |

## How to choose

- **Choose NestJS** if you want a complete, opinionated framework to build services in, with a large ecosystem and a choice of transports, and you're happy managing inter-service contract types by hand.
- **Choose @imqueue** if you specifically want typed, low-ceremony RPC between services, on Redis, without a framework dictating your app structure.
- **Consider both** if you like Nest for building a service and want `@imqueue` as the typed wire between services.

## Frequently asked questions about @imqueue and NestJS

### Is @imqueue a NestJS alternative?
Only for the narrow job of service-to-service RPC. It replaces the microservices module, not the framework — there's no DI container, no HTTP layer and no module system in `@imqueue`.

### Can I use @imqueue inside a NestJS application?
Yes. A generated client is an ordinary TypeScript class, so it can be constructed in a Nest provider and injected like anything else.

### Does NestJS's Redis transporter do the same thing as @imqueue?
They both move messages over Redis, but the contract differs: Nest routes on patterns you design and types the payload with generics you assert; `@imqueue` addresses by service name and generates the client from the service, so the compiler catches drift after a regenerate.

### Which has better type safety between services?
`@imqueue`, for the specific reason that the client is derived from the service rather than declared alongside it. The cost is a generation step and a hard dependency on JSDoc.

### Can @imqueue use Kafka or NATS?
Not out of the box — Redis is the only supported transport today, though `IMessageQueue` is the documented interface for an adapter. If your transport is already Kafka, NestJS is the pragmatic answer.

### Is NestJS heavier at runtime?
That's the wrong axis to worry about; the meaningful difference is conceptual surface, not milliseconds. Nest asks you to adopt a framework; `@imqueue` asks you to adopt a transport and a codegen step.

---

To see `@imqueue`'s model in practice, the [**Tutorial**](https://imqueue.org/tutorial/) builds a multi-service app step by step with a GraphQL gateway in front. For the typing model specifically, see [type-safe service communication in TypeScript](https://imqueue.org/blog/type-safe-service-communication-typescript/). For the other framework comparison — broader feature set, more transports, MIT licence — see [@imqueue vs Moleculer](https://imqueue.org/blog/imqueue-vs-moleculer/), or [how @imqueue compares with tRPC](https://imqueue.org/blog/imqueue-vs-trpc/) if the boundary you care about is client–server. Shipping inside a closed-source product? See [commercial licensing & support](https://imqueue.org/license/).

