InteractiveFrameworks

Hybrid applications

One application, two doors: the gateway keeps serving HTTP and also listens on TCP for the events the cats service publishes, sharing its global pipe with the messages through inheritAppConfig.

What you'll learn

  • Make an HTTP application listen on a transport too, with connectMicroservice() and startAllMicroservices(), and know the order they must run in
  • Handle messages or events in the same controllers that serve routes
  • Know that global pipes, guards, interceptors and filters apply to a connected microservice only with inheritAppConfig
  • Let a service publish events to the apps that care, with a client pointing back at them

The shelter's front page wants a live feed of adoptions, and the gateway serves the front page. But the adoption happens in the cats service, and the gateway only learns what it asks about. The service knows the moment it happens; it should tell the gateway. For that the gateway must do something a pure HTTP app cannot: listen for messages. It needs a second door, a transport, beside the HTTP one.

An application that listens on more than one transport is a hybrid application. It is the shape most real services end up with: an HTTP API for clients, plus a queue or a TCP port for other services' events.

Connecting a microservice to an app

connectMicroservice() attaches a transport listener to an existing HTTP app, and startAllMicroservices() starts every one attached:

const app = await NestFactory.create(ReportsModule);
app.connectMicroservice<MicroserviceOptions>({ transport: Transport.TCP, options: { port: 4100 } });
await app.startAllMicroservices();
await app.listen(3000);

The options are the ones createMicroservice() takes. The app is still one application: one module tree, one set of providers. A controller may carry routes and message handlers side by side, so a ReportsController can serve GET /reports and handle @EventPattern('order.placed') in the same class, with the same injected service.

connectMicroservice() only attaches. Until startAllMicroservices() has run, nothing listens on the port, and whoever sends to it gets connect ECONNREFUSED. Start the transports before listen(), so the app is fully ready once the HTTP side answers.

Global configuration is per door

app.useGlobalPipes(), useGlobalGuards(), useGlobalInterceptors() and useGlobalFilters() configure the HTTP app. A connected microservice has its own configuration and does not see them, unless it is connected with inheritAppConfig:

app.connectMicroservice<MicroserviceOptions>(
  { transport: Transport.TCP, options: { port: 4100 } },
  { inheritAppConfig: true },
);

Then both doors share one set of global enhancers. That matters more than it looks, because of what JSON does to data in transit. A Date leaves the sender as a string such as "2026-10-01T00:00:00.000Z". A payload class can declare @Type(() => Date) on the property, but only a pipe with transform: true applies it. Without that pipe the handler receives a plain object, not an instance of the class, whatever the parameter's type says.

Publishing to the apps that care

For the service to publish to the gateway, it needs a client pointing at the gateway's port, registered with ClientsModule like any other. Now each side is both server and client: the gateway sends messages to the service's port, and the service publishes events to the gateway's port. With TCP every publisher must know every listener's address, which is exactly what a broker (Redis, NATS, RabbitMQ) removes in production: publishers and listeners only know the broker.

Your task

Adoption is a message now: POST /cats/:id/adopt with { "by": "Ann", "on": "2026-10-01" } sends { cmd: 'adopt' }, so the gateway can answer 404 or 409. The gateway validates HTTP bodies with a global ValidationPipe({ transform: true }). FeedService holds the feed, GET /feed serves it, and CatAdoptedEvent describes the event, its on date declared with @Type(() => Date).

  1. In main.ts, make the gateway listen for messages too: TCP on port 3002, sharing the app's global pipe. Start it before the HTTP side listens.
  2. In the cats service, register a client named GATEWAY_EVENTS for port 3002. Once an adoption is recorded, publish 'cat.adopted' to it with { id, name, by, on }.
  3. In the gateway's FeedController, handle 'cat.adopted'. Its payload is a CatAdoptedEvent. Add <name> went home with <adopter> on <YYYY-MM-DD> to the feed, the date taken from the event's on.

When it fails

  • The feed stays empty and the console says TypeError: event.on.toISOString is not a function: the global pipe never ran on the event, so on is still a string. Connect with { inheritAppConfig: true }.
  • The feed stays empty and nothing is logged anywhere: nothing listens on 3002. Either startAllMicroservices() was never called or the ports differ. The service's publish fails with ECONNREFUSED, and an event has nobody to report that to.
  • listen EADDRINUSE: address already in use :::3000: the connected microservice was given the HTTP port.
  • Nest can't resolve dependencies of the CatsController (CatsService, ?): the GATEWAY_EVENTS client is registered in the gateway instead of the service, or not registered at all.

Remember

  • connectMicroservice() attaches a transport to an HTTP app; startAllMicroservices() starts it, before listen().
  • One app, one module tree: controllers may mix routes and message handlers.
  • Global enhancers reach a connected microservice only with inheritAppConfig: true.
  • JSON carries strings, not Dates; a transforming pipe and @Type() bring the class back.
Stuck? Show a hint

main.ts: gateway.connectMicroservice<MicroserviceOptions>({ transport: Transport.TCP, options: { port: 3002 } }, { inheritAppConfig: true }), then await gateway.startAllMicroservices() before gateway.listen(3000). Service: ClientsModule.register a 'GATEWAY_EVENTS' client on port 3002, inject it, emit('cat.adopted', { id, name, by, on }) after recording. Gateway: @EventPattern('cat.adopted') on a FeedController method taking @Payload() event: CatAdoptedEvent.