> ## Content Index
> Fetch the complete content index at: https://itsamir.ir/llms.txt
> Use this file to discover other available public pages before exploring further.

# چطور در Node.js با Inversify و gRPC یک ارتباط تمیز ساختم؟
- URL: https://itsamir.ir/nodejs-clean-architecture-grpc-inversify/
- Published: 2026-09-26T18:41:18.000Z
- Updated: 2026-09-26T18:55:33.000Z
- Description: یک راهنمای عملی برای اضافه کردن gRPC به پروژه Node.js با TypeScript، Inversify و Clean Architecture؛ از تعریف قرارداد Protobuf و ts-proto تا DI، مدیریت خطا و تست با Bruno.
- Author: ItsAmir
- Tags: TypeScript, Backend, gRPC, Microservices

> **خلاصه:**  
> مدتی بود می‌خواستم یک نمونه واقعی از ارتباط میکروسرویسی با gRPC در Node.js داشته باشم؛ نه یک Hello World ساده، بلکه چیزی که واقعاً بتوانم بعداً در پروژه‌های دیگر هم از آن استفاده کنم. در این پست، با TypeScript، Inversify، gRPC و Clean Architecture یک سرویس جمع‌وجور می‌سازیم، با گزینه‌های کاربردی ts-proto کدها را جنریت می‌کنیم و در نهایت همه‌چیز را با Bruno تست می‌کنیم.

مدتی بود حس می‌کردم جای یک پیاده‌سازی تمیز از gRPC در جعبه‌ابزار شخصی‌ام خالی است.

در پروژه‌های مختلف، از ایده‌هایی مثل [PXLAds](https://pxlads.ir/?ref=itsamir.ir) گرفته تا سرویس‌های بزرگ‌تر، بالاخره به نقطه‌ای می‌رسیم که چند سرویس باید با یکدیگر ارتباط داشته باشند.  
REST در خیلی از سناریوها کاملاً کافی است، اما وقتی ارتباط بین سرویس‌ها پررنگ‌تر می‌شود، چیزهایی مثل قرارداد مشخص (Contract)، ایمنی تایپ (Type Safety)، سربار کمتر و سرعت بالاتر در مقیاس باینری اهمیت بیشتری پیدا می‌کنند.

اینجا بود که تصمیم گرفتم یک سناریوی واقعی برای خودم پیاده کنم:  
یک اپلیکیشن Node.js با TypeScript و Inversify که بر اساس Clean Architecture ساخته شده، REST API خودش را دارد و در کنار آن یک gRPC Server هم برای ارتباط با سایر سرویس‌ها ارائه می‌دهد.

هدفم این نبود که فقط gRPC را بالا بیاورم؛ می‌خواستم ساختار طوری باشد که Domain و Use Caseهای برنامه هیچ وابستگی مستقیمی به Express یا gRPC نداشته باشند.

این پست هم نتیجه همان تمرین است و هم یک Cheat Sheet برای خودم؛ چیزی که احتمالاً چند ماه دیگر دوباره سراغش می‌آیم.

## تصویر کلی معماری

ساختار پروژه را به چهار لایه اصلی تقسیم کردم:

- **Domain:** موجودیت‌ها و منطق اصلی بیزینس، مثل `User`.
- **Application:** یوزکیس‌ها و اینترفیس‌هایی که رفتار برنامه را تعریف می‌کنند؛ مثل `RegisterUseCase` و `GetUserIdByUsernameUseCase`.
- **Infrastructure:** جزئیات فنی مثل اتصال به MongoDB و پیاده‌سازی مخزن‌ها، مثل `MongoUserRepository`.
- **Presentation:** ورودی‌های برنامه؛ یعنی هر چیزی که درخواست را از دنیای بیرون دریافت می‌کند.

در لایه Presentation فعلاً دو ورودی داریم:

1. **REST / Express** برای ارتباط با کلاینت‌های عمومی
2. **gRPC** برای ارتباط سریع بین سرویس‌ها

```text
[ Client / Microservices ]
       │            │
  (HTTP / REST)   (gRPC)
       │            │
       ▼            ▼
   Controllers   gRPC Server  ◄── Presentation Layer
          \      /
           ▼    ▼
          Use Cases           ◄── Application Layer (Inversify DI)
              │
              ▼
         Repositories         ◄── Infrastructure Layer (MongoDB)
```

نکته مهم اینجاست که هم Controllerهای REST و هم Handlerهای gRPC فقط ورودی برنامه هستند. منطق اصلی همچنان دست‌نخورده داخل Use Caseها باقی می‌ماند.

## گام اول: تعریف قرارداد با Protobuf

در gRPC همه‌چیز از یک **Contract** شروع می‌شود.  
قرارداد مشخص می‌کند چه سرویسی داریم، چه متدی ارائه می‌دهد، چه داده‌ای دریافت می‌کند و خروجی آن چیست.

برای این سناریو، فایل زیر را داخل پوشه `contracts/` ساختم:

📄 `contracts/user.proto`

```protobuf
syntax = "proto3";

package com.playground;

service GetUserId {
    rpc getUserId(GetUserIdRequest) returns (GetUserIdResponse) {}
}

message GetUserIdRequest {
    string username = 1;
}

message GetUserIdResponse {
    string id = 1;
}

```

هدف سرویس ساده و سرراست است: یک `username` دریافت می‌کنیم و در صورت وجود، `id` مربوطه را برمی‌گردانیم.

## تبدیل فایل Proto به TypeScript با گزینه‌های کاربردی

برای تولید تایپ‌ها از ابزار مدرن `ts-proto` استفاده کردم. برای اینکه خروجی دقیقاً برای محیط Node و پکیج `@grpc/grpc-js` آماده باشد، دستور زیر را اجرا کردم:

```bash
protoc \
  --plugin=./node_modules/.bin/protoc-gen-ts_proto \
  --ts_proto_out=./generated \
  --ts_proto_opt=outputServices=grpc-js,env=node \
  -I ./contracts \
  ./contracts/*.proto

```

### چرا این فلگ‌ها مهم هستند؟

- گزینه `--ts_proto_opt=outputServices=grpc-js,env=node`: به `ts-proto` می‌گوید کدها را طوری کامپایل کند که تعاریف سرویس و کلاینت مستقیماً با `@grpc/grpc-js` هماهنگ باشند.
- گزینه `-I ./contracts`: دایرکتوری مرجع را مشخص می‌کند تا ایمپورت‌ها درست حل شوند.

### یک نکته حیاتی درباره خروجی ts-proto

یکی از چیزهایی که اول کار می‌تواند گیج‌کننده باشد، نحوه برخورد با Messageها در `ts-proto` است.  
در ابزارهای سنتی مثل `google-protobuf`، پیام‌ها کلاس بودند:

```typescript
// اشتباه در ts-proto:
const response = new GetUserIdResponse();
response.setId(user.id);

```

اما در `ts-proto` خروجی‌ها **Plain TypeScript Interfaces** هستند؛ یعنی با آبجکت‌های ساده جاوااسکریپت طرفیم و به راحتی آبجکت معمولی پاس می‌دهیم:

```typescript
return callback(null, { id: result.id });

```

## گام دوم: ساخت Use Case واقعی

نمی‌خواستم سرور فقط یک مقدار هاردکدشده برگرداند؛ پس یک Use Case واقعی ساختم که بر اساس نام‌کاربری داخل ریپازیتوری جست‌وجو کند و شناسه کاربر را تحویل دهد:

📄 `src/application/use-cases/get-user-id-by-username.use-case.ts`

```typescript
import { inject, injectable } from "inversify";
import {
  IUserRepository,
  USER_REPOSITORY,
} from "../interfaces/user.repository";

type GetUserIdInput = {
  username: string;
};

type GetUserIdOutput = {
  id: string;
};

@injectable()
export class GetUserIdByUsernameUseCase {
  constructor(
    @inject(USER_REPOSITORY)
    private readonly userRepo: IUserRepository,
  ) {}

  async execute(data: GetUserIdInput): Promise<GetUserIdOutput> {
    const user = await this.userRepo.findByUsername(data.username);
    if (!user || !user.id) {
      throw new Error("User not found");
    }

    return { id: user.id };
  }
}

```

این Use Case هیچ دانشی درباره Express یا gRPC ندارد. تنها چیزی که می‌شناسد، اینترفیس `IUserRepository` است.

### ثبت Use Case در کانتینر Inversify

در فایل `src/module.ts` یوزکیس جدید را به ماژول کانتینر اضافه کردم:

```typescript
bind(GetUserIdByUsernameUseCase).toSelf().inSingletonScope();

```

## گام سوم: پیاده‌سازی سرور gRPC

حالا در لایه Presentation سرور gRPC را پیاده‌سازی می‌کنیم تا ورودی را اعتبارسنجی کند، یوزکیس را از DI بخواند و روی پورت `50051` اصطلاحاً Bind شود:

📄 `src/presentation/grpc/server.ts`

```typescript
import {
  sendUnaryData,
  Server,
  ServerCredentials,
  ServerUnaryCall,
  status,
} from "@grpc/grpc-js";

import {
  GetUserIdService,
  GetUserIdRequest,
  GetUserIdResponse,
  GetUserIdServer,
} from "../../../generated/user";
import { getContainer } from "../../container";
import { GetUserIdByUsernameUseCase } from "../../application/use-cases/get-user-id-by-username.use-case";

export function createGrpcServer(): Server {
  const server = new Server();
  const container = getContainer();
  const getUserIdByUsernameUseCase = container.get(GetUserIdByUsernameUseCase);

  const getUserIdHandler: GetUserIdServer["GetUserId"] = async (
    call: ServerUnaryCall<GetUserIdRequest, GetUserIdResponse>,
    callback: sendUnaryData<GetUserIdResponse>,
  ) => {
    try {
      const username = call.request.username;
      if (!username || username.trim() === "") {
        return callback({
          code: status.INVALID_ARGUMENT,
          message: "Username is required",
        });
      }

      console.log(
        `[gRPC] Received getUserId request for username: "${username}"`,
      );

      const result = await getUserIdByUsernameUseCase.execute({ username });
      return callback(null, { id: result.id });
    } catch (error) {
      if (error instanceof Error) {
        return callback({
          code: status.INTERNAL,
          message: "An internal server error",
        });
      }

      console.error("[GRPC error]", error);

      return callback({
        code: status.INTERNAL,
        message: "An internal server error",
      });
    }
  };

  server.addService(GetUserIdService, {
    getUserId: getUserIdHandler,
  });

  return server;
}

export function startGrpcServer(port: number = 50051): Promise<Server> {
  return new Promise((resolve, reject) => {
    const server = createGrpcServer();
    const address = `0.0.0.0:${port}`;

    server.bindAsync(
      address,
      ServerCredentials.createInsecure(),
      (err, boundPort) => {
        if (err) {
          return reject(err);
        }

        console.log(`gRPC server is running on port ${boundPort}`);
        resolve(server);
      },
    );
  });
}

```

> 💡 **نکته‌ای برای پروداکشن (Error Handling معنایی):**  
> در کد فعلی، برای سادگی در بلوک `catch` خطاهای عمومی به کد `status.INTERNAL` مپ شده‌اند. در یک پیاده‌سازی تکامل‌یافته، می‌توان خطای پیدا نشدن کاربر (مثلاً `UserNotFoundError`) را به صورت مجزا بررسی کرد و با کد گویای `status.NOT_FOUND` به کلاینت پاسخ داد تا کلاینت بداند که این خطا ناشی از کرش سرور نبوده، بلکه کاربر وجود نداشته است.

## گام چهارم: اجرای همزمان REST و gRPC در Bootstrap

پروژه از قبل سرور Express را روی پورت `2000` داشت. کافی بود در متد `bootstrap` فایل `src/index.ts` سرور gRPC را هم فراخوانی کنم:

📄 `src/index.ts`

```typescript
async function bootstrap() {
  try {
    await mongoose.connect(MONGO_URI);
    console.log("Connected to MongoDB successfully");

    app.listen(PORT, () => {
      console.log(`Server is running on port ${PORT}`);
    });

    // راه‌اندازی سرور gRPC روی پورت 50051
    await startGrpcServer(50051);
  } catch (error) {
    console.error("Failed to start application:", error);
    process.exit(1);
  }
}

bootstrap();

```

حالا یک پردازش داریم که دو پورت ورودی مجزا را همزمان ارائه می‌دهد:

- **Express / REST:** `localhost:2000`
- **gRPC Server:** `localhost:50051`

## گام پنجم: تست بی‌دردسر gRPC با Bruno

برای تست متدها نیازی نیست وقت بگذاریم و کلاینت تستی بنویسیم. نرم‌افزار **Bruno** به صورت بومی از gRPC پشتیبانی می‌کند:

1. در Bruno یک درخواست جدید از نوع **gRPC** بسازید.
2. آدرس سرور را `localhost:50051` وارد کنید (بدون SSL یا Insecure).
3. در تب **Proto File**، فایل `contracts/user.proto` پروژه را انتخاب کنید.
4. از لیست متدها، گزینه `com.playground.GetUserId / getUserId` را انتخاب کنید.

با زدن دکمه **Send**، پاسخ را در پنل روبرو مشاهده خواهید کرد:

```json
{
  "id": "6740b2a8d119bc30e..."
}

```

در تب **Body** پیام تستی را وارد کنید:

```json
{
  "username": "amir"
}

```

## چیت‌شیت سریع (Quick Cheat Sheet)

| بخش                      | دستور یا نکته کلیدی                                                                                                                   |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
| **دستور جنریت تایپ‌ها**  | protoc --plugin=... --ts\_proto\_out=./generated --ts\_proto\_opt=outputServices=grpc-js,env=node -I ./contracts ./contracts/\*.proto |
| **نوع پاسخ در ts-proto** | { id: result.id } (به عنوان POJO، بدون new)                                                                                           |
| **کد پارامتر نامعتبر**   | status.INVALID\_ARGUMENT                                                                                                              |
| **کد خطای داخلی**        | status.INTERNAL                                                                                                                       |
| **اتصال لوکال بدون SSL** | ServerCredentials.createInsecure()                                                                                                    |
| **تست مستقیم**           | Bruno با لود کردن فایل user.proto روی پورت 50051                                                                                      |

## جمع‌بندی

چیزی که در این پیاده‌سازی بیش از همه برایم ارزشمند بود، حفظ یکپارچگی معماری تمیز بود. Use Case ما اصلاً نمی‌داند که درخواست را یک مرورگر وب از درگاه REST فرستاده یا یک میکروسرویس از طریق کانال باینری gRPC. هسته برنامه در هر دو حالت ثابت مانده است.

این تمرین یکی از آن پایه‌هایی است که بعداً برای افزودن امکاناتی مثل **Streaming**، **Interceptorها** و سیستم‌های احراز هویت در ارتباطات میکروسرویسی دوباره به آن بازخواهم گشت.