This project demonstrates how to implement gRPC services using Node.js and TypeScript with service-to-service communication.
nodejs-grpc-sample/
├── README.md # This file
├── app/ # Application utilities and shared resources
│ ├── package.json # Shared dependencies
│ ├── service.proto # Protocol buffer definitions
│ └── test-services.ts # Service-to-service testing utility
├── service1/ # UserService (gRPC server)
│ ├── server.ts # UserService implementation
│ ├── package.json # Dependencies and scripts
│ └── tsconfig.json # TypeScript configuration
└── service2/ # ProductService (gRPC server)
├── server.ts # ProductService implementation
├── package.json # Dependencies and scripts
└── tsconfig.json # TypeScript configuration
GetUser- Get a user by IDCreateUser- Create a new userListUsers- List all users with paginationGetUserWithProducts- Get user with all products (calls ProductService)
GetProduct- Get a product by IDCreateProduct- Create a new productListProducts- List all products with paginationGetProductWithUsers- Get product with all users (calls UserService)
This architecture demonstrates bidirectional gRPC communication between services:
- UserService → ProductService: When
GetUserWithProductsis called, UserService calls ProductService to get all products - ProductService → UserService: When
GetProductWithUsersis called, ProductService calls UserService to get all users
- Protocol Buffers: Both services load the same
service.protofile from theapp/directory - gRPC Servers: Each service starts on its own port (50051 for UserService, 50052 for ProductService)
- Logging: Comprehensive logging tracks all requests, responses, and service-to-service calls
Direct Client Requests:
Client → gRPC Request → Service → Business Logic → Database (in-memory arrays) → gRPC Response → Client
Service-to-Service Communication:
Client → Service A → gRPC Client → Service B → Business Logic → Database → gRPC Response → Service A → Combined Response → Client
Modern Async/Await Pattern:
// Instead of callbacks:
serviceClient.method(request, (err, response) => {
if (err) { /* handle error */ }
else { /* handle response */ }
});
// We use Promise-based pattern:
const response = await new Promise((resolve, reject) => {
serviceClient.method(request, (err, response) => {
if (err) reject(err);
else resolve(response);
});
});
// Now we can use try/catch and async/awaitGetUserWithProducts Flow:
- Client calls
UserService.GetUserWithProducts({ user_id: "1" }) - UserService finds user in local array
- UserService creates gRPC client to ProductService
- UserService calls
ProductService.ListProducts({ page: 1, limit: 10 }) - ProductService returns products array
- UserService combines user + products data
- UserService returns combined response to client
Error Handling:
- gRPC Status Codes: Uses standard gRPC status codes (NOT_FOUND, INTERNAL, etc.)
- Promise Rejection: Errors are properly caught and re-thrown with context
- Comprehensive Logging: All operations are logged with timestamps and context
graph TB
subgraph "Client Layer"
Client[Client/Test Utility]
end
subgraph "Service Layer"
UserService[UserService<br/>Port 50051]
ProductService[ProductService<br/>Port 50052]
end
subgraph "Data Layer"
UserData[(Users Array)]
ProductData[(Products Array)]
end
%% Service-to-Service Communication
UserService -.->|GetUserWithProducts<br/>user_id: 1|ProductService
ProductService -.->|GetProductWithUsers<br/>product_id: 1|UserService
%% Client to Service Communication
Client -->|GetUser|UserService
Client -->|CreateUser|UserService
Client -->|ListUsers|UserService
Client -->|GetProduct|ProductService
Client -->|CreateProduct|ProductService
Client -->|ListProducts|ProductService
Client -->|GetUserWithProducts|UserService
Client -->|GetProductWithUsers|ProductService
%% Service to Data Communication
UserService -->|CRUD Operations|UserData
ProductService -->|CRUD Operations|ProductData
%% Styling
classDef service fill:#232323,stroke:#023047,color:#ffffff
classDef data fill:#f3f4f6,stroke:#6c757d,color:#000000
classDef client fill:#28a745,stroke:#1e7e34,color:#ffffff
class UserService,ProductService service
class UserData,ProductData data
class Client client
cd app
npm install
cd ..cd service1
npm install
cd ../service2
npm installFor automated development and testing, use the Auto tool:
Install Auto:
# Download the latest release binary from GitHub
# Visit: https://github.com/marcuwynu23/Auto/releases
# Download the appropriate binary for your platform
# Add the binary to your PATHAvailable Commands:
# Start both services in development mode and run tests
auto devManual Commands (without Auto):
# Development mode (with auto-reload)
cd service1 && npm run dev
cd service2 && npm run dev
cd app && npm run devImportant: Both services must be running for service-to-service communication to work.
- Start UserService (Terminal 1):
cd service1
npm startThe service will start on localhost:50051
- Start ProductService (Terminal 2):
cd service2
npm startThe service will start on localhost:50052
Use npm run dev to start with auto-reload on file changes.
The project includes a test utility that demonstrates service-to-service communication:
# Make sure both services are running, then run:
npx tsx app/test-services.tsThis will test both directions of communication:
- UserService calling ProductService
- ProductService calling UserService
Install grpcurl for testing:
go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latestTest UserService calling ProductService:
# Get user with products (UserService calls ProductService)
grpcurl -plaintext -d '{"user_id": "1"}' localhost:50051 services.UserService/GetUserWithProductsTest ProductService calling UserService:
# Get product with users (ProductService calls UserService)
grpcurl -plaintext -d '{"product_id": "1"}' localhost:50052 services.ProductService/GetProductWithUsersUserService operations:
# Get a user
grpcurl -plaintext -d '{"user_id": "1"}' localhost:50051 services.UserService/GetUser
# List all users
grpcurl -plaintext -d '{"page": 1, "limit": 10}' localhost:50051 services.UserService/ListUsersProductService operations:
# Get a product
grpcurl -plaintext -d '{"product_id": "1"}' localhost:50052 services.ProductService/GetProduct
# List all products
grpcurl -plaintext -d '{"page": 1, "limit": 10}' localhost:50052 services.ProductService/ListProductsThe service.proto file contains the service definitions and message types for both services. Both services share the same proto file to ensure type safety and consistency.
Both services come with pre-populated sample data:
- User ID 1: John Doe (john@example.com, age 30)
- User ID 2: Jane Smith (jane@example.com, age 25)
- Product ID 1: Laptop ($1299.99)
- Product ID 2: Mouse ($49.99)
- Uses TypeScript for type safety
- Uses ES modules (
"type": "module"in package.json) - Uses
tsxfor running TypeScript files directly - In-memory data storage (data resets on server restart)
- gRPC servers use insecure credentials (for development only)
To extend this example:
- Add database persistence
- Implement authentication/authorization
- Add SSL/TLS for secure communication
- Add streaming RPCs
- Add interceptors for logging/metrics
- Add comprehensive error handling