Next.js starter your AI actually understands. Ship internal tools in days not weeks. Pre-order $199 $499 → [Get it now]

How to expose gRPC in Node.js

Exposing gRPC services in Node.js enables building high-performance microservices that communicate using protocol buffers. As the creator of CoreUI with over 10 years of Node.js experience since 2014, I’ve built gRPC servers for real-time data processing, inter-service communication, and high-throughput APIs. The standard approach uses @grpc/grpc-js to create a server, define service implementations, and bind them to network ports. This provides type-safe, efficient service endpoints that outperform traditional REST APIs for many use cases.

Install @grpc/grpc-js and @grpc/proto-loader to create gRPC servers.

npm install @grpc/grpc-js @grpc/proto-loader

These packages provide the core gRPC server functionality. The @grpc/grpc-js is the official pure JavaScript implementation. The @grpc/proto-loader loads .proto files and generates service definitions at runtime.

Defining a Proto File

Create a service contract in a .proto file.

// user.proto
syntax = "proto3";

package user;

service UserService {
  rpc GetUser (UserRequest) returns (UserResponse);
  rpc CreateUser (CreateUserRequest) returns (UserResponse);
  rpc ListUsers (Empty) returns (stream UserResponse);
}

message UserRequest {
  string id = 1;
}

message CreateUserRequest {
  string name = 1;
  string email = 2;
}

message UserResponse {
  string id = 1;
  string name = 2;
  string email = 3;
}

message Empty {}

This proto file defines a UserService with three RPC methods. The GetUser and CreateUser are unary RPCs (single request, single response). The ListUsers is a server streaming RPC (single request, multiple responses). Protocol buffers provide strongly typed contracts.

Creating a gRPC Server

Build a server that implements the service methods.

const grpc = require('@grpc/grpc-js')
const protoLoader = require('@grpc/proto-loader')

const PROTO_PATH = './user.proto'

const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
  keepCase: true,
  longs: String,
  enums: String,
  defaults: true,
  oneofs: true
})

const userProto = grpc.loadPackageDefinition(packageDefinition).user

const users = [
  { id: '1', name: 'John Doe', email: '[email protected]' },
  { id: '2', name: 'Jane Smith', email: '[email protected]' }
]

function getUser(call, callback) {
  const user = users.find(u => u.id === call.request.id)
  if (user) {
    callback(null, user)
  } else {
    callback({
      code: grpc.status.NOT_FOUND,
      details: 'User not found'
    })
  }
}

function createUser(call, callback) {
  const newUser = {
    id: String(users.length + 1),
    name: call.request.name,
    email: call.request.email
  }
  users.push(newUser)
  callback(null, newUser)
}

function listUsers(call) {
  users.forEach(user => {
    call.write(user)
  })
  call.end()
}

const server = new grpc.Server()

server.addService(userProto.UserService.service, {
  getUser,
  createUser,
  listUsers
})

server.bindAsync(
  '0.0.0.0:50051',
  grpc.ServerCredentials.createInsecure(),
  (error, port) => {
    if (error) {
      console.error('Failed to bind server:', error)
      return
    }
    console.log(`Server running on port ${port}`)
  }
)

The getUser function handles unary requests with a callback. The createUser function modifies data and returns the result. The listUsers function streams multiple responses using call.write(). The server.addService() registers the implementation. The bindAsync() starts the server on port 50051.

Implementing Unary RPC Methods

Handle single request/response RPCs with validation.

function getUser(call, callback) {
  const { id } = call.request

  if (!id) {
    return callback({
      code: grpc.status.INVALID_ARGUMENT,
      details: 'User ID is required'
    })
  }

  const user = users.find(u => u.id === id)

  if (!user) {
    return callback({
      code: grpc.status.NOT_FOUND,
      details: `User with ID ${id} not found`
    })
  }

  callback(null, user)
}

The function extracts the request data from call.request. Validation checks ensure required fields are present. Errors are returned with appropriate gRPC status codes. Success responses pass null as the first argument and data as the second. This follows Node.js error-first callback conventions.

Implementing Server Streaming

Stream multiple responses to a single request.

function listUsers(call) {
  const { limit = 10, offset = 0 } = call.request

  const paginatedUsers = users.slice(offset, offset + limit)

  paginatedUsers.forEach((user, index) => {
    setTimeout(() => {
      call.write(user)
      if (index === paginatedUsers.length - 1) {
        call.end()
      }
    }, index * 100)
  })
}

The call.write() method sends each user as a separate message. The call.end() signals the stream is complete. The timeout simulates async processing - in production, this might involve database queries or API calls. Server streaming is efficient for large datasets.

Implementing Client Streaming

Accept multiple requests and return a single response.

// Add to proto file
// rpc BatchCreateUsers (stream CreateUserRequest) returns (BatchCreateResponse);

function batchCreateUsers(call, callback) {
  const createdUsers = []

  call.on('data', (request) => {
    const newUser = {
      id: String(users.length + createdUsers.length + 1),
      name: request.name,
      email: request.email
    }
    createdUsers.push(newUser)
  })

  call.on('end', () => {
    users.push(...createdUsers)
    callback(null, {
      count: createdUsers.length,
      users: createdUsers
    })
  })

  call.on('error', (error) => {
    console.error('Stream error:', error)
  })
}

The data event fires for each incoming request. The end event fires when the client finishes sending. The callback returns a summary of created users. This pattern is efficient for bulk operations.

Adding Middleware and Error Handling

Implement global error handling and logging.

function wrapHandler(handler) {
  return async (call, callback) => {
    try {
      console.log(`[${new Date().toISOString()}] ${handler.name} called`)

      if (handler.length === 1) {
        // Streaming handler
        return handler(call)
      }

      // Unary handler
      await handler(call, callback)
    } catch (error) {
      console.error(`Error in ${handler.name}:`, error)
      callback({
        code: grpc.status.INTERNAL,
        details: 'Internal server error'
      })
    }
  }
}

server.addService(userProto.UserService.service, {
  getUser: wrapHandler(getUser),
  createUser: wrapHandler(createUser),
  listUsers: wrapHandler(listUsers)
})

The wrapper function logs all requests and catches unhandled errors. It distinguishes between unary and streaming handlers by checking function arity. This provides centralized error handling and observability.

Using SSL/TLS for Production

Secure the gRPC server with SSL/TLS certificates.

const fs = require('fs')

const serverCert = fs.readFileSync('./certs/server-cert.pem')
const serverKey = fs.readFileSync('./certs/server-key.pem')
const caCert = fs.readFileSync('./certs/ca-cert.pem')

const serverCredentials = grpc.ServerCredentials.createSsl(
  caCert,
  [{
    private_key: serverKey,
    cert_chain: serverCert
  }],
  true
)

server.bindAsync(
  '0.0.0.0:50051',
  serverCredentials,
  (error, port) => {
    if (error) {
      console.error('Failed to bind server:', error)
      return
    }
    console.log(`Secure server running on port ${port}`)
  }
)

The createSsl() method configures TLS with certificate files. The CA certificate validates clients. The server certificate and key authenticate the server. The true parameter requires client authentication. This ensures encrypted, authenticated communication.

Best Practice Note

This is the same gRPC server pattern we use in CoreUI backend services for building scalable microservice architectures. Always use SSL/TLS in production - never deploy with createInsecure() credentials. Implement proper error handling with meaningful gRPC status codes to help clients handle failures. Add request validation before processing to prevent invalid data. For high-load scenarios, consider implementing connection pooling and load balancing across multiple server instances. gRPC provides significant performance benefits over REST for service-to-service communication, especially when combined with HTTP/2 multiplexing and protocol buffer efficiency.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author

Subscribe to our newsletter
Get early information about new products, product updates and blog posts.

Answers by CoreUI Core Team