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
Basic Client-Server Implementation
Learn how to build a complete JSON-RPC client-server system with practical examples.
Overview
This guide demonstrates how to implement a complete JSON-RPC 2.0 client-server system using Node.js and Express. You'll learn how to create a server that handles multiple methods and a client that can communicate with it.
What You'll Build
- JSON-RPC 2.0 compliant server
- Client with request/notification support
- Error handling and validation
- Multiple method types
- Batch request support
Technologies Used
- Node.js & Express.js
- JSON-RPC 2.0 specification
- JavaScript ES6+
- HTTP POST requests
- Error handling patterns
Server Implementation
Let's start by creating a JSON-RPC server that can handle various types of requests.
server.js
const express = require('express');
const app = express();
const PORT = 3000;
// Middleware
app.use(express.json());
// CORS middleware for development
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Headers', 'Content-Type');
res.header('Access-Control-Allow-Methods', 'POST');
next();
});
// In-memory data store
let users = [
{ id: 1, name: 'Alice', email: '[email protected]' },
{ id: 2, name: 'Bob', email: '[email protected]' }
];
let eventLog = [];
// Helper function to create JSON-RPC response
function createResponse(id, result, error = null) {
const response = { jsonrpc: '2.0', id };
if (error) {
response.error = error;
} else {
response.result = result;
}
return response;
}
// Helper function to create error objects
function createError(code, message, data = null) {
const error = { code, message };
if (data) error.data = data;
return error;
}
// JSON-RPC method handlers
const methods = {
// Math operations
add(params) {
if (!Array.isArray(params) || params.length !== 2) {
throw createError(-32602, 'Invalid params',
{ expected: 'array of 2 numbers', received: params });
}
const [a, b] = params;
if (typeof a !== 'number' || typeof b !== 'number') {
throw createError(-32602, 'Invalid params',
{ expected: 'numbers', received: typeof a + ', ' + typeof b });
}
return a + b;
},
subtract(params) {
if (!Array.isArray(params) || params.length !== 2) {
throw createError(-32602, 'Invalid params');
}
const [a, b] = params;
if (typeof a !== 'number' || typeof b !== 'number') {
throw createError(-32602, 'Invalid params');
}
return a - b;
},
multiply(params) {
if (!Array.isArray(params) || params.length !== 2) {
throw createError(-32602, 'Invalid params');
}
const [a, b] = params;
if (typeof a !== 'number' || typeof b !== 'number') {
throw createError(-32602, 'Invalid params');
}
return a * b;
},
divide(params) {
if (!Array.isArray(params) || params.length !== 2) {
throw createError(-32602, 'Invalid params');
}
const [a, b] = params;
if (typeof a !== 'number' || typeof b !== 'number') {
throw createError(-32602, 'Invalid params');
}
if (b === 0) {
throw createError(-32000, 'Division by zero');
}
return a / b;
},
// User management
getUser(params) {
if (!params || typeof params.id !== 'number') {
throw createError(-32602, 'Invalid params',
{ expected: 'object with id property', received: params });
}
const user = users.find(u => u.id === params.id);
if (!user) {
throw createError(-32001, 'User not found', { id: params.id });
}
return user;
},
getUsers() {
return users;
},
createUser(params) {
if (!params || !params.name || !params.email) {
throw createError(-32602, 'Invalid params',
{ expected: 'object with name and email', received: params });
}
const newUser = {
id: Math.max(...users.map(u => u.id)) + 1,
name: params.name,
email: params.email
};
users.push(newUser);
return newUser;
},
updateUser(params) {
if (!params || typeof params.id !== 'number') {
throw createError(-32602, 'Invalid params');
}
const userIndex = users.findIndex(u => u.id === params.id);
if (userIndex === -1) {
throw createError(-32001, 'User not found', { id: params.id });
}
if (params.name) users[userIndex].name = params.name;
if (params.email) users[userIndex].email = params.email;
return users[userIndex];
},
deleteUser(params) {
if (!params || typeof params.id !== 'number') {
throw createError(-32602, 'Invalid params');
}
const userIndex = users.findIndex(u => u.id === params.id);
if (userIndex === -1) {
throw createError(-32001, 'User not found', { id: params.id });
}
const deletedUser = users.splice(userIndex, 1)[0];
return { success: true, user: deletedUser };
},
// Utility methods
echo(params) {
return params;
},
getTime() {
return new Date().toISOString();
},
// Notification handlers (no response)
logEvent(params) {
const event = {
...params,
timestamp: new Date().toISOString()
};
eventLog.push(event);
console.log('Event logged:', event);
return true; // Won't be returned for notifications
},
getEventLog() {
return eventLog;
}
};
// Main JSON-RPC handler
app.post('/jsonrpc', (req, res) => {
try {
// Handle batch requests
if (Array.isArray(req.body)) {
const responses = [];
for (const request of req.body) {
const response = processRequest(request);
// Only add response if it's not a notification
if (response !== null) {
responses.push(response);
}
}
// If all requests were notifications, return 204 No Content
if (responses.length === 0) {
return res.status(204).end();
}
return res.json(responses);
}
// Handle single request
const response = processRequest(req.body);
// If it's a notification, return 204 No Content
if (response === null) {
return res.status(204).end();
}
res.json(response);
} catch (error) {
console.error('Unhandled error:', error);
res.json(createResponse(null, null, createError(-32603, 'Internal error')));
}
});
// Process individual JSON-RPC request
function processRequest(request) {
// Validate JSON-RPC format
if (!request || request.jsonrpc !== '2.0' || !request.method) {
return createResponse(request?.id || null, null,
createError(-32600, 'Invalid Request'));
}
const { method, params, id } = request;
const isNotification = id === undefined;
try {
// Check if method exists
if (!methods[method]) {
const error = createError(-32601, 'Method not found',
{ method, available: Object.keys(methods) });
return isNotification ? null : createResponse(id, null, error);
}
// Execute method
const result = methods[method](params);
// Return response (or null for notifications)
return isNotification ? null : createResponse(id, result);
} catch (error) {
// Handle method execution errors
if (isNotification) {
console.error(`Error in notification ${method}:`, error);
return null;
}
return createResponse(id, null, error);
}
}
// Health check endpoint
app.get('/health', (req, res) => {
res.json({ status: 'OK', timestamp: new Date().toISOString() });
});
// Start server
app.listen(PORT, () => {
console.log(`JSON-RPC server running on http://localhost:${PORT}`);
console.log('Available methods:', Object.keys(methods));
});
Client Implementation
Now let's create a flexible JSON-RPC client that can send requests and notifications.
client.js
class JsonRpcClient {
constructor(url, options = {}) {
this.url = url;
this.options = options;
this.requestId = 1;
this.timeout = options.timeout || 5000;
}
// Generate unique request ID
generateId() {
return this.requestId++;
}
// Send HTTP request
async sendRequest(payload) {
try {
const response = await fetch(this.url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...this.options.headers
},
body: JSON.stringify(payload),
signal: AbortSignal.timeout(this.timeout)
});
// For notifications, expect 204 No Content
if (response.status === 204) {
return null;
}
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
} catch (error) {
if (error.name === 'AbortError') {
throw new Error('Request timeout');
}
throw error;
}
}
// Make a regular JSON-RPC call (expects response)
async call(method, params) {
const request = {
jsonrpc: '2.0',
method,
id: this.generateId()
};
if (params !== undefined) {
request.params = params;
}
const response = await this.sendRequest(request);
if (response.error) {
const error = new Error(response.error.message);
error.code = response.error.code;
error.data = response.error.data;
throw error;
}
return response.result;
}
// Send a notification (no response expected)
async notify(method, params) {
const notification = {
jsonrpc: '2.0',
method
};
if (params !== undefined) {
notification.params = params;
}
await this.sendRequest(notification);
}
// Send batch request
async batch(requests) {
const batchRequest = requests.map(req => {
const request = {
jsonrpc: '2.0',
method: req.method
};
if (req.params !== undefined) {
request.params = req.params;
}
// Add ID only if it's not a notification
if (!req.notification) {
request.id = this.generateId();
}
return request;
});
const response = await this.sendRequest(batchRequest);
// Handle case where all requests were notifications
if (response === null) {
return [];
}
// Process batch response
const results = [];
for (const res of response) {
if (res.error) {
const error = new Error(res.error.message);
error.code = res.error.code;
error.data = res.error.data;
results.push({ error });
} else {
results.push({ result: res.result });
}
}
return results;
}
}
// Example usage
async function runExamples() {
const client = new JsonRpcClient('http://localhost:3000/jsonrpc');
try {
console.log('=== Basic Math Operations ===');
// Basic arithmetic
const sum = await client.call('add', [10, 5]);
console.log('10 + 5 =', sum);
const difference = await client.call('subtract', [10, 5]);
console.log('10 - 5 =', difference);
const product = await client.call('multiply', [10, 5]);
console.log('10 * 5 =', product);
const quotient = await client.call('divide', [10, 5]);
console.log('10 / 5 =', quotient);
console.log('\n=== User Management ===');
// Get all users
const users = await client.call('getUsers');
console.log('All users:', users);
// Get specific user
const user = await client.call('getUser', { id: 1 });
console.log('User 1:', user);
// Create new user
const newUser = await client.call('createUser', {
name: 'Charlie',
email: '[email protected]'
});
console.log('Created user:', newUser);
// Update user
const updatedUser = await client.call('updateUser', {
id: newUser.id,
name: 'Charlie Brown'
});
console.log('Updated user:', updatedUser);
console.log('\n=== Utility Methods ===');
// Echo test
const echo = await client.call('echo', { message: 'Hello, World!' });
console.log('Echo:', echo);
// Get server time
const time = await client.call('getTime');
console.log('Server time:', time);
console.log('\n=== Notifications ===');
// Send some notifications (no response)
await client.notify('logEvent', {
type: 'user_action',
action: 'login',
userId: 1
});
console.log('Login event notification sent');
await client.notify('logEvent', {
type: 'system',
action: 'backup_started'
});
console.log('Backup event notification sent');
// Check event log
const eventLog = await client.call('getEventLog');
console.log('Event log:', eventLog);
console.log('\n=== Batch Requests ===');
// Send batch request
const batchResults = await client.batch([
{ method: 'add', params: [1, 2] },
{ method: 'multiply', params: [3, 4] },
{ method: 'getTime' },
{ method: 'logEvent', params: { type: 'batch_test' }, notification: true }
]);
console.log('Batch results:', batchResults);
console.log('\n=== Error Handling ===');
try {
await client.call('nonexistentMethod');
} catch (error) {
console.log('Expected error:', error.message, '(Code:', error.code + ')');
}
try {
await client.call('divide', [10, 0]);
} catch (error) {
console.log('Division by zero error:', error.message);
}
try {
await client.call('getUser', { id: 999 });
} catch (error) {
console.log('User not found error:', error.message);
}
} catch (error) {
console.error('Unexpected error:', error);
}
}
// Run examples if this script is executed directly
if (require.main === module) {
runExamples();
}
module.exports = JsonRpcClient;
Package Dependencies
package.json
{
"name": "jsonrpc-example",
"version": "1.0.0",
"description": "Basic JSON-RPC client-server example",
"main": "server.js",
"scripts": {
"start": "node server.js",
"client": "node client.js",
"dev": "nodemon server.js"
},
"dependencies": {
"express": "^4.18.2"
},
"devDependencies": {
"nodemon": "^3.0.1"
},
"keywords": ["jsonrpc", "json-rpc", "api", "server", "client"],
"author": "Your Name",
"license": "MIT"
}
Running the Example
Step 1: Setup
# Create project directory
mkdir jsonrpc-example
cd jsonrpc-example
# Initialize npm project
npm init -y
# Install dependencies
npm install express
npm install -D nodemon
# Create the files
# (Copy server.js and client.js code from above)
Step 2: Start the Server
# Start the server
npm start
# Or with auto-reload during development
npm run dev
The server will start on http://localhost:3000
Step 3: Run the Client
# In a new terminal, run the client
npm run client
# Or directly
node client.js
Expected Output
=== Basic Math Operations ===
10 + 5 = 15
10 - 5 = 5
10 * 5 = 50
10 / 5 = 2
=== User Management ===
All users: [
{ id: 1, name: 'Alice', email: '[email protected]' },
{ id: 2, name: 'Bob', email: '[email protected]' }
]
User 1: { id: 1, name: 'Alice', email: '[email protected]' }
Created user: { id: 3, name: 'Charlie', email: '[email protected]' }
Updated user: { id: 3, name: 'Charlie Brown', email: '[email protected]' }
=== Utility Methods ===
Echo: { message: 'Hello, World!' }
Server time: 2024-01-15T10:30:00.123Z
=== Notifications ===
Login event notification sent
Backup event notification sent
Event log: [
{
type: 'user_action',
action: 'login',
userId: 1,
timestamp: '2024-01-15T10:30:00.123Z'
},
{
type: 'system',
action: 'backup_started',
timestamp: '2024-01-15T10:30:00.456Z'
}
]
=== Batch Requests ===
Batch results: [
{ result: 3 },
{ result: 12 },
{ result: '2024-01-15T10:30:00.789Z' }
]
=== Error Handling ===
Expected error: Method not found (Code: -32601)
Division by zero error: Division by zero
User not found error: User not found
Key Features Demonstrated
Server Features
- JSON-RPC 2.0 compliance
- Request validation
- Error handling with proper codes
- Notification support
- Batch request processing
- CORS support for web clients
- Method parameter validation
Client Features
- Promise-based API
- Request timeout handling
- Error parsing and throwing
- Notification sending
- Batch request support
- Automatic ID generation
- Flexible parameter handling
Browser Usage
The client can also be used in browsers. Here's a simplified browser-compatible version:
browser-client.js
class JsonRpcClient {
constructor(url) {
this.url = url;
this.requestId = 1;
}
async call(method, params) {
const request = {
jsonrpc: '2.0',
method,
id: this.requestId++
};
if (params !== undefined) {
request.params = params;
}
const response = await fetch(this.url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(request)
});
const data = await response.json();
if (data.error) {
throw new Error(`${data.error.message} (Code: ${data.error.code})`);
}
return data.result;
}
async notify(method, params) {
const notification = { jsonrpc: '2.0', method };
if (params !== undefined) notification.params = params;
await fetch(this.url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(notification)
});
}
}
// Usage in browser
const client = new JsonRpcClient('http://localhost:3000/jsonrpc');
// Example: Add two numbers
client.call('add', [5, 3]).then(result => {
console.log('5 + 3 =', result);
});
// Example: Send notification
client.notify('logEvent', {
type: 'user_action',
action: 'button_click'
});
Next Steps
Enhanced Features
- Add authentication
- Implement rate limiting
- Add request logging
- Set up SSL/TLS
- Add input sanitization
Production Ready
- Add database integration
- Implement proper error handling
- Add monitoring and metrics
- Set up clustering
- Add comprehensive tests
Advanced Topics
- WebSocket transport
- Real-time notifications
- Load balancing
- Microservices integration
- API versioning