Skip to content
NestJS ns microservices 4 min read

Hybrid Applications

A hybrid application is a NestJS application that listens for requests from multiple sources at once: a regular HTTP server alongside one or more microservice transports (TCP, Redis, NATS, RabbitMQ, Kafka, and so on). Instead of deploying a separate process for every transport, you bootstrap a single application that serves REST/GraphQL traffic and consumes messages or events from a broker. This is the pragmatic choice for many real-world services that expose a public HTTP API while also reacting to internal messaging.

How a hybrid application works

You start from a normal HTTP application created with NestFactory.create(). That gives you a Nest application instance backed by an HTTP adapter (Express or Fastify). You then attach microservice listeners to that same instance by calling connectMicroservice() once per transport. Each call returns a microservice instance, but the important effect is that the listener is registered against the shared application context — the same module graph, the same DI container, the same providers.

Because everything shares one container, a @Controller() handling HTTP routes and an @Controller() decorated with @MessagePattern() can both inject the same singleton services. You write your business logic once and expose it through whichever entry point fits.

The microservice instances created by connectMicroservice() only begin listening after you call startAllMicroservices(). Forgetting that call is the single most common reason “my message handlers never fire.”

Bootstrapping with connectMicroservice

The bootstrap sequence has three distinct phases: connect the microservice transports, start them, then start the HTTP server.

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

async function bootstrap() {
  // 1. Create the HTTP application as usual
  const app = await NestFactory.create(AppModule);

  // 2. Attach one or more microservice transports
  app.connectMicroservice<MicroserviceOptions>({
    transport: Transport.TCP,
    options: { host: '0.0.0.0', port: 3001 },
  });

  app.connectMicroservice<MicroserviceOptions>({
    transport: Transport.REDIS,
    options: { host: 'localhost', port: 6379 },
  });

  // 3. Start all attached microservices, then the HTTP listener
  await app.startAllMicroservices();
  await app.listen(3000);

  console.log('HTTP on :3000, TCP on :3001, Redis subscriber active');
}
bootstrap();

Output:

[Nest] 4821  - 06/14/2026, 9:41:02 AM     LOG [NestFactory] Starting Nest application...
[Nest] 4821  - 06/14/2026, 9:41:02 AM     LOG [NestMicroservice] Nest microservice successfully started
[Nest] 4821  - 06/14/2026, 9:41:02 AM     LOG [NestMicroservice] Nest microservice successfully started
[Nest] 4821  - 06/14/2026, 9:41:02 AM     LOG [NestApplication] Nest application successfully started
HTTP on :3000, TCP on :3001, Redis subscriber active

Sharing modules and providers

The whole point of a hybrid app is reuse. Define your domain logic in an injectable service and consume it from both an HTTP controller and a message controller.

// src/orders/orders.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class OrdersService {
  private readonly orders = new Map<string, { id: string; total: number }>();

  create(id: string, total: number) {
    const order = { id, total };
    this.orders.set(id, order);
    return order;
  }

  findOne(id: string) {
    return this.orders.get(id);
  }
}
// src/orders/orders.controller.ts
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
import { EventPattern, MessagePattern, Payload } from '@nestjs/microservices';
import { OrdersService } from './orders.service';

@Controller('orders')
export class OrdersController {
  constructor(private readonly orders: OrdersService) {}

  // HTTP entry point
  @Post()
  createHttp(@Body() body: { id: string; total: number }) {
    return this.orders.create(body.id, body.total);
  }

  @Get(':id')
  getHttp(@Param('id') id: string) {
    return this.orders.findOne(id);
  }

  // Microservice request/response entry point — same service instance
  @MessagePattern('orders.get')
  getMessage(@Payload() id: string) {
    return this.orders.findOne(id);
  }

  // Microservice fire-and-forget event
  @EventPattern('orders.created')
  onCreated(@Payload() data: { id: string; total: number }) {
    this.orders.create(data.id, data.total);
  }
}

A single controller can mix HTTP route handlers and message/event handlers because all decorators register against the same Nest instance. The injected OrdersService is one shared singleton.

Configuration options

connectMicroservice() accepts a second, optional argument that controls how the listener integrates with the host application:

OptionTypeDefaultPurpose
inheritAppConfigbooleanfalseApply HTTP-level global pipes, filters, interceptors, and guards to microservice handlers too
(first arg) transportTransportThe transport strategy (TCP, REDIS, NATS, RMQ, KAFKA, GRPC)
(first arg) optionsobject{}Transport-specific connection options
app.connectMicroservice<MicroserviceOptions>(
  { transport: Transport.NATS, options: { servers: ['nats://localhost:4222'] } },
  { inheritAppConfig: true }, // reuse global pipes/filters/interceptors
);

By default, global pipes/filters registered with app.useGlobalPipes() apply only to HTTP handlers. Pass { inheritAppConfig: true } so your validation and exception handling also wrap message handlers — otherwise you must register them again on the microservice.

Graceful shutdown

Hybrid apps respond to shutdown hooks across all transports. Enable them so both the HTTP server and every broker connection close cleanly.

const app = await NestFactory.create(AppModule);
app.connectMicroservice<MicroserviceOptions>({ transport: Transport.TCP });
app.enableShutdownHooks();
await app.startAllMicroservices();
await app.listen(3000);

Best Practices

  • Always call startAllMicroservices() before app.listen() so brokers are subscribed before HTTP traffic arrives.
  • Keep business logic in shared @Injectable() providers; let controllers be thin adapters over HTTP and message transports.
  • Pass inheritAppConfig: true when you want global validation pipes and exception filters to cover message handlers too.
  • Use app.enableShutdownHooks() to drain broker connections and HTTP requests gracefully on SIGTERM.
  • Prefer a hybrid app when transports share state or deployment lifecycle; split into separate processes when you need independent scaling.
  • Bind TCP/Redis ports to distinct values from your HTTP port, and document them clearly for ops and health checks.
Last updated June 14, 2026
Was this helpful?