visit
For sure, it depends on multiple factors including team expertise, what problem should be solved, which scale is expected, etc…
This is a very simple example that represents separation of responsibilities and data segregation. Here we have a concept of an account
which is a combination of a user
and a profile
. Let’s imagine we have a generic users
service which is responsible only for authentication. For security reasons, we must not store credentials and personal data in the same db or even manage them in the same service. That’s why we have a dedicated profiles
service. It’s just a story but our imaginary client somehow should get an account
which contains data both from user
and profile
. That’s why we need a BFF (backend for frontend) service which will merge data from different microservices and return back to the client.
First let’s install nest-cli
and generate nest projects:
$ mkdir nestjs-microservices && cd nestjs-microservices
$ npm install -g @nestjs/cli
$ nest new users
$ nest new profiles
$ nest new bff
By default NestJS generates Http server, so let’s update users
and profiles
to make them communicate through an event based protocol, e.g.: Redis Pub/Sub:
$ npm i --save @nestjs/microservices
/src/main.ts
const app = await NestFactory.createMicroservice<MicroserviceOptions>(
AppModule,
{
transport: Transport.REDIS,
options: {
url: 'redis://localhost:6379',
},
},
);
app.listen(() => console.log('Services started'));
A NestJS service consists of modules and modules are split into different parts, but the most important of them are controllers and services. Controllers are responsible for providing an external service API. Let’s define them for users
and profiles
services.
/users/src/app.controller.ts
@Controller()
export class AppController {
@MessagePattern({ cmd: 'get_users' })
getUsers() {
return this.appService.getUsers();
}
}
/users/src/app.service.ts
@Injectable()
export class AppService {
users = [
{ id: '1', login: 'bob' },
{ id: '2', login: 'john' },
];
getUsers(): User[] {
return this.users;
}
}
/profiles/src/app.controller.ts
@Controller()
export class AppController {
@MessagePattern({ cmd: 'get_profiles' })
getProfiles() {
return this.appService.getProfiles();
}
}
/profiles/src/app.service.ts
@Injectable()
export class AppService {
profiles = [
{ id: '1', name: 'Bob' },
{ id: '2', name: 'John' },
];
getProfiles(): Profile[] {
return this.profiles;
}
}
/bff/src/app.controller.ts
@Controller()
export class AppController {
constructor(
@Inject('PUBSUB')
private readonly client: ClientProxy,
) {}
@Get('accounts')
async getAccounts(): Promise<Account[]> {
const users = await this.client
.send<User[]>({ cmd: 'get_users' }, { page: 1, items: 10 })
.toPromise();
const profiles = await this.client
.send<Profile[]>({ cmd: 'get_profiles' }, { ids: users.map((u) => u.id) })
.toPromise();
return users.map<Account>((u) => ({
...u,
...profiles.find((p) => p.id === u.id),
}));
}
}
/bff/src/app.module.ts
@Module({
imports: [
ClientsModule.register([
{
name: 'PUBSUB',
transport: Transport.REDIS,
options: {
url: 'redis://localhost:6379',
},
},
]),
],
controllers: [AppController],
providers: [AppService, Pubsub],
})
export class AppModule {}
npm run start:dev
curl //localhost:3000/accounts | jq
[
{
"id": 1,
"login": "bob",
"name": "Bob"
},
{
"id": 2,
"login": "john",
"name": "John"
}
]
I’ve used the following two tools: curl
and jq
. You can always use your preferred tools or just follow the article and install them using any package manager you’re comfortable with.
@MessagePattern()
- for synchronous messages style@EventPattern()
- for asynchronous messages style
Let’s implement case #3. This time we will use emit()
method and @EventPattern()
decorator.
/bff/src/app.controller.ts
@Post('accounts')
async createAccount(@Body() account: Account): Promise<void> {
await this.client.emit({ cmd: 'create_account' }, account);
}
/profiles/src/app.controller.ts
@EventPattern({ cmd: 'create_account' })
createProfile(profile: Profile): Profile {
return this.appService.createProfile(profile);
}
/profiles/src/app.service.ts
createProfile(profile: Profile): Profile {
this.profiles.push(profile);
return profile;
}
/users/src/app.controller.ts
@EventPattern({ cmd: 'create_account' })
createUser(account: Account): User {
const {id, login} = account;
return this.appService.createUser({id, login});
}
/users/src/app.service.ts
createProfile(user: User): User {
this.users.push(user);
return user;
}
curl -X POST http:/localhost:3000/accounts \
-H "accept: application/json" -H "Content-Type: application/json" \
--data "{'id': '3', 'login': 'jack', 'name': 'Jack'}" -i
HTTP/1.1 201 Created
curl //localhost:3000/accounts | jq
[
{
"id": "1",
"login": "bob",
"name": "Bob"
},
{
"id": "2",
"login": "john",
"name": "John"
},
{
"id": "3",
"login": "jack",
"name": "Jack"
}
]