JSON-RPC Basic Client-Server Example

This example demonstrates how to create a simple JSON-RPC 2.0 server using Express.js and a client that communicates with it. You'll learn the fundamental patterns for implementing JSON-RPC in a JavaScript environment.

What You'll Learn

  • How to set up a basic JSON-RPC server using Express.js
  • How to implement method handlers on the server
  • How to create a client that sends JSON-RPC requests
  • How to handle responses and errors
  • The JSON-RPC 2.0 message structure and protocol flow

1. Server Implementation

First, let's create a simple JSON-RPC server using Express.js. This server will expose methods for basic arithmetic operations: addition, subtraction, multiplication, and division.

Start by installing the required dependencies:

npm install express body-parser

Now, create a file named server.js with the following code:

server.jsExpress.js JSON-RPC Server
const express = require('express');
const bodyParser = require('body-parser');

const app = express();
const PORT = 3000;

// Middleware to parse JSON request body
app.use(bodyParser.json());

// Process JSON-RPC requests
app.post('/jsonrpc', (req, res) => {
  const request = req.body;
  
  // Validate JSON-RPC request
  if (!request.jsonrpc || request.jsonrpc !== '2.0' || !request.method) {
    return res.json({
      jsonrpc: '2.0',
      error: {
        code: -32600,
        message: 'Invalid Request'
      },
      id: request.id || null
    });
  }

  // Process method calls
  let result;
  let error;

  try {
    switch (request.method) {
      case 'add':
        if (!Array.isArray(request.params) || request.params.length !== 2) {
          throw { code: -32602, message: 'Invalid params' };
        }
        result = request.params[0] + request.params[1];
        break;
        
      case 'subtract':
        if (!Array.isArray(request.params) || request.params.length !== 2) {
          throw { code: -32602, message: 'Invalid params' };
        }
        result = request.params[0] - request.params[1];
        break;
        
      case 'multiply':
        if (!Array.isArray(request.params) || request.params.length !== 2) {
          throw { code: -32602, message: 'Invalid params' };
        }
        result = request.params[0] * request.params[1];
        break;
        
      case 'divide':
        if (!Array.isArray(request.params) || request.params.length !== 2) {
          throw { code: -32602, message: 'Invalid params' };
        }
        if (request.params[1] === 0) {
          throw { code: -32602, message: 'Division by zero' };
        }
        result = request.params[0] / request.params[1];
        break;
        
      default:
        throw { code: -32601, message: 'Method not found' };
    }
  } catch (e) {
    error = {
      code: e.code || -32603,
      message: e.message || 'Internal error'
    };
  }

  // Build the response
  const response = {
    jsonrpc: '2.0',
    id: request.id
  };

  if (error) {
    response.error = error;
  } else {
    response.result = result;
  }

  res.json(response);
});

// Start the server
app.listen(PORT, () => {
  console.log(`JSON-RPC server running at http://localhost:${PORT}`);
});

In a production environment, you should add proper logging, input validation, and error handling. This example focuses on the JSON-RPC protocol implementation.

Key Components of the Server:

  1. Endpoint Setup: We create a single POST endpoint /jsonrpc that handles all RPC requests.
  2. Request Validation: We verify that the request follows the JSON-RPC 2.0 specification (has jsonrpc: '2.0' and a method).
  3. Method Dispatch: Based on the method field, we route to the appropriate function.
  4. Parameter Validation: We check that the parameters match what's expected for each method.
  5. Error Handling: We use standard JSON-RPC error codes and formats.
  6. Response Structure: We ensure every response includes jsonrpc: '2.0' and the same id as the request.

2. Client Implementation

Now let's create a simple client that can communicate with our JSON-RPC server. Create a file named client.js:

client.jsJSON-RPC Client
const fetch = require('node-fetch');

class JsonRpcClient {
  constructor(url) {
    this.url = url;
    this.id = 1;
  }

  async call(method, params) {
    const request = {
      jsonrpc: '2.0',
      method,
      params,
      id: this.id++
    };

    const response = await fetch(this.url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(request)
    });

    const result = await response.json();

    if (result.error) {
      throw new Error(`Error ${result.error.code}: ${result.error.message}`);
    }

    return result.result;
  }
}

// Create client instance
const client = new JsonRpcClient('http://localhost:3000/jsonrpc');

// Example usage
async function runExamples() {
  try {
    // Addition
    const sum = await client.call('add', [10, 5]);
    console.log('10 + 5 =', sum);

    // Subtraction
    const difference = await client.call('subtract', [10, 5]);
    console.log('10 - 5 =', difference);

    // Multiplication
    const product = await client.call('multiply', [10, 5]);
    console.log('10 * 5 =', product);

    // Division
    const quotient = await client.call('divide', [10, 5]);
    console.log('10 / 5 =', quotient);

    // Error case - division by zero
    try {
      await client.call('divide', [10, 0]);
    } catch (e) {
      console.log('Error caught:', e.message);
    }

    // Error case - method not found
    try {
      await client.call('power', [2, 3]);
    } catch (e) {
      console.log('Error caught:', e.message);
    }
  } catch (e) {
    console.error('Error:', e);
  }
}

// Run the examples
runExamples();

Key Components of the Client:

  1. JsonRpcClient Class: A simple class that handles creating and sending JSON-RPC requests.
  2. Request Construction: We create a properly formatted JSON-RPC 2.0 request with all required fields.
  3. ID Management: The client automatically increments the request ID to match requests with responses.
  4. Error Handling: We check for error responses and throw them as JavaScript exceptions.
  5. Method Examples: We demonstrate calling each method and handling various error cases.

3. Running the Example

To run this example, you'll need to:

  1. Save both files (server.js and client.js) in the same directory
  2. Install the required packages with npm install express body-parser node-fetch
  3. Start the server in one terminal with node server.js
  4. Run the client in another terminal with node client.js

You should see the following output from the client:

10 + 5 = 15 10 - 5 = 5 10 * 5 = 50 10 / 5 = 2 Error caught: Error -32602: Division by zero Error caught: Error -32601: Method not found

4. Key Concepts

JSON-RPC Message Structure

Every JSON-RPC 2.0 request must include:

  • jsonrpc: Must be exactly "2.0"
  • method: The name of the method to call
  • params: An array or object of parameters (optional)
  • id: A unique identifier for matching requests and responses

Error Handling

JSON-RPC defines standard error codes:

  • -32600: Invalid Request
  • -32601: Method not found
  • -32602: Invalid params
  • -32603: Internal error
  • -32000 to -32099: Server error (reserved)

Best Practices

  • Use a single endpoint for all methods to simplify your API
  • Implement proper error handling with standard error codes
  • Consider using named parameters (object) instead of positional parameters (array) for clarity
  • Always include the id from the request in your response
  • Validate all input parameters before processing
  • Use HTTPS in production for secure communication