Your First NestJS Microservice: Building an API Gateway and a User Service
Welcome back, microservice adventurers! In the previous part, we got cozy with the "why" behind microservices, the magic of NestJS, and set up our foundational Nx monorepo. You've got a shiny new workspace, and your api-gateway is saying "Hello World!" – which is great, but it's a bit lonely, isn't it?
Today, we're going to give our api-gateway a friend: our very first dedicated microservice, the users-service. This is where the rubber meets the road, and you'll see how independent services start to communicate, laying the groundwork for our full microservice ecosystem.
The Gatekeeper: Understanding the API Gateway
Before we dive into code, let's chat briefly about the API Gateway. In a microservice architecture, the API Gateway acts as the single, unified entry point for all external clients (like your web browser, mobile app, or another third-party system). Instead of clients needing to know the complex network addresses and specific endpoints of all your individual microservices, they simply make requests to the gateway. This single point of contact significantly simplifies client-side development and overall system management.
Think of it like a concierge at a fancy, sprawling hotel. As a guest, you don't need to know which specific department handles your laundry, room service, or spa booking. You simply tell the concierge what you need, and they efficiently direct your request to the right specialized team within the hotel. Similarly, your clients interact only with the API Gateway, which then intelligently routes their requests to the correct backend microservice.
The API Gateway does a lot more than just routing, though; it's a powerful hub for cross-cutting concerns:
- Request Routing: Its primary and most fundamental job is to inspect incoming client requests and forward them to the appropriate backend microservice. For example, a request to
/usersmight go to theusers-service, while a request to/productsgoes to theproducts-service. This abstraction means clients don't need to know the internal topology of your microservices. - Request Aggregation: Imagine a client needs data from three different microservices to display a single page (e.g., user profile from
users-service, recent orders fromorders-service, and loyalty points fromloyalty-service). The API Gateway can receive one request from the client, make parallel calls to these three services, combine their responses, and then send a single, consolidated response back to the client. This reduces network round-trips for the client and simplifies client-side logic. - Authentication/Authorization: Often, the gateway handles initial security checks. Instead of every microservice needing to validate user tokens or permissions, the API Gateway can perform this once for all incoming requests. If a request is unauthorized, it's rejected at the gateway, preventing malicious or unauthenticated traffic from even reaching your backend services. We'll dive deep into implementing JWT-based authentication here in a later part of this series.
- Rate Limiting, Caching, Logging: These are common concerns that apply to almost all requests. Implementing them at the API Gateway centralizes their management. For instance, you can easily apply a global rate limit to prevent abuse, cache frequently requested data to reduce load on backend services, or log all incoming requests for monitoring and debugging purposes, all from a single, consistent point.
- Protocol Translation: This is a subtle but powerful feature. Your clients might communicate with the API Gateway using standard RESTful HTTP APIs. However, internally, your microservices might communicate using different, more efficient protocols like gRPC (which we'll explore next!) or message queues. The API Gateway can seamlessly translate between these protocols, exposing a consistent interface to the outside world while allowing your internal services to use the most optimal communication methods.
For our setup, the api-gateway will be a standard NestJS HTTP application that exposes our public API endpoints. When a request comes in for user-related data, it won't handle that logic itself. Instead, it will forward that request to our dedicated users-service.
Building Our First Microservice: The users-service
Time to create our users-service! Thanks to Nx, this is super straightforward. Make sure you're in the root of your nestjs-ms-blueprint monorepo.
Step 1: Generate the users-service Application
We'll use the Nx NestJS application generator, just like we did for the api-gateway.
nx g @nx/nest:app users-serviceWhen prompted for the application name, type users-service. You can again choose No Css for styling, as this is a backend service.
After this command runs, you'll see a new directory inside your apps/ folder: users-service. This is a completely independent NestJS application, ready to become our first microservice. Nx ensures it has its own project.json and tsconfig.app.json, allowing it to be built and run in isolation from other applications in the monorepo.
Step 2: Install Microservices Package
For NestJS applications to function as microservices (either as clients that send messages or servers that receive them), we need to install the @nestjs/microservices package. This package provides the core functionalities and abstractions for inter-service communication. Navigate into your new users-service directory and install it:
cd apps/users-service
npm install @nestjs/microservices
# or yarn add @nestjs/microservices
cd ../../ # Go back to the monorepo rootStep 3: Configure users-service as a Microservice Server
Now, let's tell our users-service to act as a microservice server, listening for incoming messages. For our first communication, we'll use the simple TCP (Transmission Control Protocol) transporter. TCP is a fundamental network protocol that provides reliable, ordered, and error-checked delivery of a stream of bytes between applications. In the context of microservices, it's a straightforward way to establish direct, point-to-point communication. It's a great starting point to understand the basic request-response pattern before we move to more advanced protocols.
Open apps/users-service/src/main.ts and modify it like so:
// apps/users-service/src/main.ts
import { NestFactory } from '@nestjs/core';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
import { AppModule } from './app/app.module'; // Adjust path if your app.module is directly in src
async function bootstrap() {
// Create a NestJS microservice instance. This is distinct from NestFactory.create()
// which would create a standard HTTP server. createMicroservice() sets up the
// application to listen for messages via a specified transport layer.
const app = await NestFactory.createMicroservice<MicroserviceOptions>(AppModule, {
transport: Transport.TCP, // Specify TCP as the transport layer. This means our
// users-service will communicate over raw TCP sockets.
options: {
host: '127.0.0.1', // Listen on localhost. This ensures the service is only
// accessible from the local machine for now, which is perfect
// for development. In production, you might bind to '0.0.0.0'.
port: 3001, // Listen on port 3001. It's absolutely crucial that each
// microservice in your system listens on a unique port to
// avoid conflicts and allow them to run simultaneously.
},
});
// Start the microservice and listen for incoming messages. This call is
// asynchronous and will keep the application running, waiting for connections.
await app.listen();
console.log('Users Microservice is listening on port 3001');
}
bootstrap();Explanation:
NestFactory.createMicroservice(): This is the key. UnlikeNestFactory.create()which boots up a standard HTTP server (like ourapi-gateway),createMicroservice()configures our application to operate in a microservice context. It sets up the necessary infrastructure to listen for messages over a specific transport layer, rather than handling HTTP requests directly.transport: Transport.TCP: We explicitly tell NestJS to use the TCP protocol for communication. This is a good default for simple, direct service-to-service communication within a controlled environment. It's efficient but lacks some of the built-in features (like schema enforcement) that other protocols, like gRPC, provide.options: { host: '127.0.0.1', port: 3001 }: We define where our microservice will listen. Thehostspecifies the network interface to bind to, and theportis the specific numerical address. It's crucial that each microservice listens on a unique port within your system to avoid conflicts and allow them to run concurrently.
Step 4: Create a Simple Message Handler in users-service
Microservices communicate using "message patterns." These are like specific commands or topics that a service can listen for. Think of them as a contract: the client sends a message with a particular pattern, and the server knows exactly which function to execute based on that pattern. This is different from traditional REST where you typically map HTTP verbs (GET, POST) and URLs to controller methods.
Let's create a basic message handler in our users-service to respond to a "get all users" request.
Open apps/users-service/src/app/app.controller.ts and update it:
// apps/users-service/src/app/app.controller.ts
import { Controller } from '@nestjs/common';
import { MessagePattern } from '@nestjs/microservices';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
// This message handler listens for the 'get_users' pattern.
// When a message arrives with this specific pattern, NestJS will
// automatically route it to this method for processing.
@MessagePattern('get_users')
getUsers(): any[] { // We'll return a simple array of mock users for now.
// In a real-world scenario, this method would interact
// with a database to fetch actual user data.
console.log('Users Microservice received request for get_users');
return [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' },
{ id: 3, name: 'Charlie', email: 'charlie@example.com' },
];
}
}Explanation:
@MessagePattern('get_users'): This decorator is the heart of microservice message handling in NestJS. It tells NestJS that this method (getUsers) should be invoked whenever an incoming message matches the string'get_users'. This provides a clear, pattern-based way for services to expose their capabilities.- The method returns a simple array of mock user objects. This is just for demonstration purposes. In a production application, this
getUsersmethod would contain the business logic to retrieve actual user data, likely from a database (which we'll set up in a later part!).
You can leave apps/users-service/src/app/app.service.ts and apps/users-service/src/app/app.module.ts as they were generated for now, as they already correctly wire up AppController and AppService within the users-service application.
Connecting from the API Gateway: The Microservice Client
Now that our users-service is ready to listen for messages, our api-gateway needs a way to send messages to it. This is where NestJS's ClientsModule comes in. It allows us to configure and manage connections to other microservices.
Step 1: Install Microservices Package in api-gateway
Just like with users-service, our api-gateway needs the microservices package to act as a client. This package provides the ClientProxy class and other utilities necessary for sending messages to microservices.
cd apps/api-gateway
npm install @nestjs/microservices
# or yarn add @nestjs/microservices
cd ../../ # Go back to the monorepo rootStep 2: Register the users-service Client in api-gateway
We'll register our users-service as a client in the api-gateway's AppModule. This tells NestJS how to connect to the users-service and makes the client injectable throughout our gateway application.
Open apps/api-gateway/src/app/app.module.ts and modify it:
// apps/api-gateway/src/app/app.module.ts
import { Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [
// ClientsModule.register is used to configure one or more microservice clients.
ClientsModule.register([
{
name: 'USERS_SERVICE', // This is a unique injection token for our client.
// We'll use this string to tell NestJS which configured
// client instance we want to inject into our controllers/services.
transport: Transport.TCP, // Specify TCP, matching the users-service server's transport.
options: {
host: '127.0.0.1', // Must match the host of the users-service.
port: 3001, // Must match the port of the users-service.
// These options define the network address where the
// api-gateway expects to find the users-service.
},
},
]),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}Explanation:
ClientsModule.register(): This static method allows us to configure one or more microservice clients within our NestJS application. It takes an array of client configurations.name: 'USERS_SERVICE': This string acts as an injection token. When we want to use this client in a controller or service, we'll use this exact string with the@Inject()decorator to tell NestJS which configuredClientProxyinstance to provide. It's a good practice to use a descriptive, uppercase string for this token.transport: Transport.TCPandoptions: These must exactly match the configuration of ourusers-serviceserver (hostandport). This is how theapi-gatewayknows precisely where and how to establish a connection with theusers-service.
Step 3: Inject and Use the Client in api-gateway
Now, let's modify our api-gateway's controller. When an HTTP request comes into the gateway for user data, we'll intercept it and, instead of handling the logic locally, we'll forward that request as a microservice message to our users-service.
Open apps/api-gateway/src/app/app.controller.ts and update it:
// apps/api-gateway/src/app/app.controller.ts
import { Controller, Get, Inject } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
import { AppService } from './app.service';
import { Observable } from 'rxjs'; // Import Observable from rxjs, as microservice calls return Observables
@Controller()
export class AppController {
constructor(
private readonly appService: AppService,
// Inject the ClientProxy using the token we defined in AppModule.
// NestJS's Dependency Injection system automatically provides the
// configured ClientProxy instance for 'USERS_SERVICE' here.
@Inject('USERS_SERVICE') private readonly usersServiceClient: ClientProxy,
) {}
// This HTTP GET endpoint will serve as the public entry point for fetching users.
// It will then trigger an internal microservice call.
@Get('users')
getUsers(): Observable<any[]> { // The return type is Observable because NestJS microservice
// client methods (like .send()) return RxJS Observables.
// The API Gateway will automatically subscribe to this
// Observable and send the resulting data as an HTTP response.
console.log('API Gateway received HTTP request for /users');
// Send a message with the 'get_users' pattern to the users-service.
// The .send() method is used for request-response communication, meaning
// the API Gateway expects a response back from the users-service.
return this.usersServiceClient.send('get_users', {}); // The second argument is the payload
// (data) to send. It's empty for now,
// but we could send parameters like
// a user ID: .send('get_user_by_id', { id: 123 }).
}
// Keep the original /hello endpoint if you want, or remove it
@Get()
getData() {
return this.appService.getData();
}
}Explanation:
@Inject('USERS_SERVICE') private readonly usersServiceClient: ClientProxy: This uses NestJS's powerful dependency injection system. By using@Inject()with thenametoken we defined inClientsModule.register(), NestJS automatically provides us with aClientProxyinstance that's pre-configured to communicate with ourusers-service. This keeps our controller clean and focused on its responsibilities.this.usersServiceClient.send('get_users', {}): This is the core of our inter-service communication!send(): This method is specifically designed for request-response communication. It sends a message to the target microservice and then waits for a response. If you just wanted to fire-and-forget an event without expecting a direct reply, you'd useemit().'get_users': This is the exact message pattern that ourusers-serviceis listening for via its@MessagePattern('get_users')decorator. This forms the explicit contract between the two services.{}: This is the payload, or the data, that we're sending along with the message pattern. For this simple "get all users" request, the payload is empty, but for operations like "get user by ID" or "create user," this would contain the relevant data.
Observable<any[]>: It's important to note that NestJS microservice client methods (likesend()) return RxJS Observables. This is a powerful reactive programming concept. When you return an Observable from an HTTP controller method in NestJS, the framework automatically subscribes to it, waits for the data to be emitted, and then sends that data back as the HTTP response to the client. This makes asynchronous operations feel incredibly seamless.
Time to See It in Action!
This is the moment of truth! We need to run both our microservices independently. Open two separate terminal windows, ensuring both are at the root of your nestjs-ms-blueprint monorepo.
Terminal 1 (for users-service):
nx serve users-serviceYou should see output similar to: Users Microservice is listening on port 3001. This confirms your users-service is up and ready to receive messages.
Terminal 2 (for api-gateway):
nx serve api-gatewayYou should see output similar to: 🚀 Application is running on: http://localhost:3000/api. This indicates your api-gateway (our HTTP server) is active and ready to accept external requests.
Now, open your web browser or use a tool like Postman/Insomnia and navigate to:
http://localhost:3000/users
What should happen? Let's trace the flow of the request:
- Your browser (or Postman/Insomnia) sends an HTTP GET request to the
api-gatewayon port 3000. This is the external client's interaction. - The
api-gateway'sAppControllerreceives the/usersHTTP request. - Inside the
getUsersmethod of theapi-gateway'sAppController, theusersServiceClient(our microservice client) is invoked. It then sends a microservice message with the pattern'get_users'to theusers-service(which is listening on port 3001). This is the internal, inter-service communication. - The
users-servicereceives the'get_users'message. Its@MessagePattern('get_users')handler inAppControlleris automatically invoked, and it returns the array of mock users. - The
api-gatewayreceives this response back from theusers-servicethrough theClientProxy's Observable. - Finally, the
api-gatewaytakes this received data and sends it back as the HTTP response to your browser.
You should see the following JSON response in your browser:
[
{ "id": 1, "name": "Alice", "email": "alice@example.com" },
{ "id": 2, "name": "Bob", "email": "bob@example.com" },
{ "id": 3, "name": "Charlie", "email": "charlie@example.com" }
]And if you check your terminal windows, you should see the console.log messages from both services, confirming that the communication flow happened exactly as expected! This visual confirmation is incredibly helpful when debugging distributed systems.
Wrapping Up Part 2
Congratulations! You've just built and connected your first two NestJS microservices within an Nx monorepo. This is a huge milestone and a fundamental step in mastering distributed system design! You now have a functioning setup where an external HTTP request hits your API Gateway, which then intelligently forwards that request to a specialized microservice using a TCP-based request-response pattern. This demonstrates the core principle of service decomposition and inter-service communication.
While TCP is simple and effective for direct service-to-service calls, it's just one piece of the puzzle. It's great for quick, internal communication, but it doesn't offer the strong contract enforcement or performance benefits that other protocols can provide, especially when dealing with complex data structures or high-volume traffic.
In the next part, we're going to level up our communication game significantly.
Get ready for Part 3: High-Speed Communication: Connecting Your Microservices with gRPC! We'll explore a much more performant, language-agnostic, and structured way for our services to talk, leveraging the power of Protocol Buffers and gRPC. It's going to be fast, fun, and incredibly valuable for building truly robust microservice architectures!
See you there!
