InteractiveFrameworks

Your first microservice

Split the cats API in two: a cats service that answers messages over TCP, and an HTTP gateway that asks it. Start the service with createMicroservice, answer patterns with @MessagePattern, register a client with ClientsModule and send it messages.

What you'll learn

  • Explain what a microservice is in Nest: an application whose transport is not HTTP, answering messages matched by pattern
  • Start a TCP microservice with NestFactory.createMicroservice and a port, and know why it must listen before anything calls it
  • Answer request-response messages with @MessagePattern in a controller, with object or string patterns
  • Register a ClientProxy with ClientsModule.register, inject it by name, and send() messages: a cold Observable, returned from a route or awaited with firstValueFrom

The cats API has grown into one application that does everything: it keeps the cats, validates adoptions, signs people in, documents itself. The shelter now wants the cats' records owned by one small team and reachable by other internal programs (the adoption desk, the vet's scheduler), not only by the public website. The usual answer is to split the app. A cats service keeps the records and answers questions about them, and a gateway faces the internet, speaks HTTP, and asks the service whenever it needs a cat.

The two are separate programs, so they need a way to talk that is not a browser request. In Nest a microservice is an application whose transport is not HTTP: it receives messages over a transport (TCP here, or Redis, NATS, Kafka and others in production) and answers them. Everything else you know still applies: modules, providers, controllers, dependency injection.

Starting a microservice

NestFactory.create() makes an HTTP app. NestFactory.createMicroservice() makes one that listens on a transport:

const app = await NestFactory.createMicroservice<MicroserviceOptions>(MathModule, {
  transport: Transport.TCP,
  options: { port: 4000 },
});
await app.listen();

listen() takes no port: the options already name it. TCP's default port is 3000, the same as the default HTTP port, so give a service its own.

In production the service and the gateway each have their own main.ts and run on different machines. The browser runs one program, so this lesson's main.ts starts both, the service first. The network between them is simulated inside this page, but the transport code that runs is Nest's own TCP client and server: the same messages, the same errors.

Answering messages

A microservice's controller has no routes. Its methods answer patterns, plain values that travel with every message, marked with @MessagePattern():

@Controller()
export class MathController {
  @MessagePattern({ cmd: 'sum' })
  accumulate(numbers: number[]): number {
    return (numbers || []).reduce((a, b) => a + b, 0);
  }
}

A pattern can be an object, as here, or a string such as 'math.sum'; both sides only have to agree on it. The handler's first argument is the message's payload, the data the caller sent. It can return a value, a promise or an Observable. Whatever it returns is serialized to JSON and sent back.

Asking: the client

The calling side needs a ClientProxy. ClientsModule.register() creates one per entry and makes it injectable under the entry's name:

@Module({
  imports: [
    ClientsModule.register([
      { name: 'MATH_SERVICE', transport: Transport.TCP, options: { port: 4000 } },
    ]),
  ],
})
export class ReportsModule {}

The name is a token, not a class, so it is injected with @Inject('MATH_SERVICE') private readonly math: ClientProxy. The client is lazy: it connects on the first message and keeps the connection.

send(pattern, payload) returns an Observable of the reply. It is cold, so nothing is sent until something subscribes. A route handler may return it as is, because Nest subscribes and answers with the value. When you need the reply in your own code, turn it into a promise:

const total = await firstValueFrom(this.math.send<number>({ cmd: 'sum' }, [1, 2, 3]));

The payload may not be undefined or null; send {} when there is nothing to say.

Only JSON crosses the wire. A class instance arrives as a plain object, a Date as a string, and a number that was a string when it left stays a string.

Your task

The service's data (CatsService) and module are in place, and the gateway has an empty controller.

  1. In main.ts, start CatsServiceModule as a TCP microservice on port 3001, and wait until it listens before the gateway starts.
  2. In the service's CatsController, answer { cmd: 'find-all' } with every cat, and { cmd: 'find-one' } with the cat whose id is the payload. The service gives null for an id it does not know.
  3. In GatewayModule, register a TCP client named CATS_SERVICE pointing at port 3001.
  4. In CatsApiController, inject that client. GET /cats answers with the reply to { cmd: 'find-all' }. GET /cats/:id sends { cmd: 'find-one' } with the id as a number, and answers 404 Cat <id> not found when the reply is null.

Try the gateway before step 2 and read the console: the client connects, and the service says it has nothing for that pattern.

When it fails

  • Error: connect ECONNREFUSED 127.0.0.1:3001 and a 500: nothing listens on 3001. Either the service was never created, its listen() was never awaited, or the ports differ.
  • There is no matching message handler defined in the remote service.: the service is up but no @MessagePattern equals the pattern that was sent. { cmd: 'find-all' } and { cmd: 'findAll' } are different patterns.
  • listen EADDRINUSE: address already in use :::3000: the microservice has no options.port, so it took TCP's default, 3000, and the gateway could not have it.
  • Every GET /cats/:id is a 404: the id was sent as the string "2", and the service compares it with the number 2. Parse it before it leaves (ParseIntPipe).
  • Nest can't resolve dependencies of the CatsApiController (?): a client is registered under a name, so it is injected with @Inject('CATS_SERVICE'), not by its type.

Remember

  • A microservice is a Nest app on a transport other than HTTP; createMicroservice() and listen() start it.
  • @MessagePattern(pattern) answers a message; the payload is the handler's first argument.
  • ClientsModule.register() gives a ClientProxy under a name; send() is cold, firstValueFrom() waits for the reply.
  • Only JSON crosses the wire: send numbers as numbers, and never undefined.
Stuck? Show a hint

main.ts: NestFactory.createMicroservice<MicroserviceOptions>(CatsServiceModule, { transport: Transport.TCP, options: { port: 3001 } }), then await its listen(). The service's controller: @MessagePattern({ cmd: 'find-all' }) on a method; the payload is the method's first argument. The gateway: ClientsModule.register([{ name, transport, options }]) in imports, @Inject('CATS_SERVICE') a ClientProxy, send(pattern, payload) with a payload that is not undefined, firstValueFrom() where you need the reply itself.