JSON-RPC Notifications Example
This example demonstrates how to implement and use JSON-RPC 2.0 notifications - a special type of request that doesn't require a response from the server, enabling fire-and-forget communication patterns.
What You'll Learn
- What JSON-RPC notifications are and when to use them
- How to send notifications from a client
- How to handle notifications on the server
- Best practices for implementing notification-based systems
- Performance benefits of using notifications
1. What are JSON-RPC Notifications?
In the JSON-RPC 2.0 specification, a notification is a special type of request that does not require a response from the server. Notifications are identified by the absence of an id
field in the request.
Key characteristics of notifications:
- No
id
field is present in the request - The server must not reply to a notification
- Client cannot know if the notification was processed successfully
- Useful for "fire-and-forget" scenarios where no acknowledgment is needed
When to Use Notifications
Notifications are ideal for:
- Logging events without waiting for confirmation
- Sending analytics data to the server
- Broadcasting status updates
- Any scenario where you don't need to confirm receipt or handle errors
// Regular JSON-RPC request{
"jsonrpc": "2.0",
"method": "updateUser",
"params": { "userId": 123, "name": "John Doe" },
"id": 1
}// JSON-RPC notification (no id field){
"jsonrpc": "2.0",
"method": "logEvent",
"params": { "event": "user_login", "userId": 123 }
}
2. Server Implementation
Let's create a server that can handle both regular JSON-RPC requests and notifications. This server will have special logic to recognize and process notifications without generating responses.
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());
// Simple in-memory log for demonstration
const eventLog = [];
// JSON-RPC request handler
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
});
}
// Check if it's a notification (no id field)
const isNotification = request.id === undefined;
// Process method calls
let result;
let error;
try {
switch (request.method) {
// Regular methods that return a response
case 'echo':
result = request.params;
break;
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;
// Methods often used as notifications
case 'logEvent':
if (!request.params || !request.params.event) {
throw { code: -32602, message: 'Invalid params, event is required' };
}
// Add timestamp to the event
const logEntry = {
...request.params,
timestamp: new Date().toISOString()
};
// Store in our log
eventLog.push(logEntry);
console.log('Event logged:', logEntry);
result = true;
break;
case 'updateStatus':
if (!request.params || !request.params.status) {
throw { code: -32602, message: 'Invalid params, status is required' };
}
console.log('Status updated:', request.params.status);
result = true;
break;
default:
throw { code: -32601, message: 'Method not found' };
}
} catch (e) {
error = {
code: e.code || -32603,
message: e.message || 'Internal error'
};
}
// For notifications, don't send a response
if (isNotification) {
// Log errors from notifications for debugging
if (error) {
console.error('Error processing notification:', error);
}
// End the request without a response body
return res.status(204).end();
}
// For regular requests, build and send a response
const response = {
jsonrpc: '2.0',
id: request.id
};
if (error) {
response.error = error;
} else {
response.result = result;
}
res.json(response);
});
// Endpoint to view the event log (for demonstration purposes)
app.get('/log', (req, res) => {
res.json(eventLog);
});
// Start the server
app.listen(PORT, () => {
console.log(`JSON-RPC server running at http://localhost:${PORT}`);
});
While we return HTTP 204 (No Content) for notifications in this example, the JSON-RPC spec only requires that no response be sent. In a real-world implementation, you might want to batch process notifications asynchronously for better performance.
Key Server Implementation Details:
- Notification Detection: We check for the absence of an
id
field to identify notifications. - Response Handling: For notifications, we return HTTP 204 (No Content) rather than a JSON-RPC response.
- Error Logging: We still log errors from notifications server-side, even though we don't return them to the client.
- Dual-Purpose Methods: Methods like
logEvent
can be called both as regular requests or as notifications.
3. Client Implementation
Now let's create a client that can send both regular requests and notifications to our server:
const fetch = require('node-fetch');
class JsonRpcClient {
constructor(url) {
this.url = url;
this.id = 1;
}
// Method for regular JSON-RPC requests (with response)
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)
});
// Parse the JSON response
const result = await response.json();
if (result.error) {
throw new Error(`Error ${result.error.code}: ${result.error.message}`);
}
return result.result;
}
// Method for sending notifications (no response expected)
async notify(method, params) {
const notification = {
jsonrpc: '2.0',
method,
params
// No id field for notifications
};
const response = await fetch(this.url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(notification)
});
// We don't expect a response body, but we should check the status
if (response.status !== 204) {
console.warn(`Unexpected status code ${response.status} for notification`);
}
// No return value for notifications
return;
}
}
// Create client instance
const client = new JsonRpcClient('http://localhost:3000/jsonrpc');
// Example usage
async function runExamples() {
try {
// Regular request - we wait for a response
console.log('Sending a regular request...');
const result = await client.call('add', [5, 3]);
console.log('Result of 5 + 3:', result);
// Send some notifications - we don't wait for responses
console.log('\nSending notifications...');
// Log a user login event
await client.notify('logEvent', {
event: 'user_login',
userId: 123,
details: { ip: '192.168.1.1', browser: 'Chrome' }
});
console.log('Login event notification sent');
// Update application status
await client.notify('updateStatus', {
status: 'active',
component: 'backend',
load: 0.75
});
console.log('Status update notification sent');
// Send another regular request to view the log
console.log('\nChecking server log...');
const logUrl = 'http://localhost:3000/log';
const logResponse = await fetch(logUrl);
const logData = await logResponse.json();
console.log('Server event log:', JSON.stringify(logData, null, 2));
} catch (e) {
console.error('Error:', e);
}
}
// Run the examples
runExamples();
Key Client Implementation Details:
- Dual Methods: We implement both
call()
for regular requests andnotify()
for notifications. - No ID for Notifications: We intentionally omit the
id
field when sending notifications. - Status Check: For notifications, we check that the server returns HTTP 204 (No Content) but don't expect any response body.
- No Return Value: The
notify()
method doesn't return a result since we don't get one from the server.
4. Running the Example
To run this example:
- Save both files (
server.js
andclient.js
) in the same directory - Install the required packages with
npm install express body-parser node-fetch
- Start the server in one terminal with
node server.js
- Run the client in another terminal with
node client.js
You should see output similar to this:
Sending a regular request...
Result of 5 + 3: 8
Sending notifications...
Login event notification sent
Status update notification sent
Checking server log...
Server event log: [
{
"event": "user_login",
"userId": 123,
"details": {
"ip": "192.168.1.1",
"browser": "Chrome"
},
"timestamp": "2023-08-15T14:52:30.123Z"
}
]
At the same time, in the server console, you'll see:
JSON-RPC server running at http://localhost:3000
Event logged: {
event: 'user_login',
userId: 123,
details: { ip: '192.168.1.1', browser: 'Chrome' },
timestamp: '2023-08-15T14:52:30.123Z'
}
Status updated: active
5. Best Practices for Notifications
When to Use Notifications
- For events that don't need acknowledgment
- For logging and analytics purposes
- For status updates that aren't critical
- When you want to optimize network performance
When NOT to Use Notifications
- For operations where success confirmation is needed
- When error handling is important
- For critical operations (e.g., financial transactions)
- When you need the result of the operation
Performance Considerations
- Notifications can reduce network overhead since no response is returned
- Consider batching multiple notifications together for even better performance
- Process notifications asynchronously on the server when possible
- Use a queue system for high-volume notifications to prevent server overload
Error Handling Strategy
Since clients cannot receive error responses for notifications, consider these strategies:
- Log errors server-side for debugging
- Implement a periodic health check request (not a notification) to verify the system is functioning
- For critical operations, use regular requests instead of notifications
- Consider implementing a separate feedback channel (e.g., WebSockets) for important systems
6. Next Steps
Now that you understand JSON-RPC notifications, you might want to explore: