rust
65 lines · 8 steps
Building a JSON-RPC 2.0 handler in Axum
An Axum handler that parses, dispatches, and answers single or batched JSON-RPC 2.0 calls with spec-compliant responses.
Explained by
highlit
1use axum::{extract::State, response::IntoResponse, Json};
2use serde::{Deserialize, Serialize};
3use serde_json::{json, Value};
4use std::sync::Arc;
5
6#[derive(Deserialize)]
7struct RpcRequest {
8 jsonrpc: String,
9 method: String,
10 #[serde(default)]
11 params: Value,
12 id: Option<Value>,
13}
14
15#[derive(Serialize)]
16struct RpcResponse {
17 jsonrpc: &'static str,
18 #[serde(skip_serializing_if = "Option::is_none")]
19 result: Option<Value>,
20 #[serde(skip_serializing_if = "Option::is_none")]
21 error: Option<Value>,
22 id: Value,
23}
24
25async fn dispatch(state: &AppState, req: RpcRequest) -> RpcResponse {
26 let id = req.id.clone().unwrap_or(Value::Null);
27 if req.jsonrpc != "2.0" {
28 return RpcResponse { jsonrpc: "2.0", result: None, id, error: Some(json!({ "code": -32600, "message": "Invalid Request" })) };
29 }
30 let outcome = match req.method.as_str() {
31 "account.balance" => state.balance(req.params).await,
32 "account.transfer" => state.transfer(req.params).await,
33 "ping" => Ok(json!("pong")),
34 other => Err(json!({ "code": -32601, "message": format!("Method not found: {other}") })),
35 };
36 match outcome {
37 Ok(result) => RpcResponse { jsonrpc: "2.0", result: Some(result), error: None, id },
38 Err(error) => RpcResponse { jsonrpc: "2.0", result: None, error: Some(error), id },
39 }
40}
41
42pub async fn rpc_handler(State(state): State<Arc<AppState>>, Json(payload): Json<Value>) -> impl IntoResponse {
43 match payload {
44 Value::Array(batch) if !batch.is_empty() => {
45 let mut responses = Vec::with_capacity(batch.len());
46 for entry in batch {
47 match serde_json::from_value::<RpcRequest>(entry) {
48 Ok(req) => responses.push(dispatch(&state, req).await),
49 Err(_) => responses.push(RpcResponse {
50 jsonrpc: "2.0", result: None, id: Value::Null,
51 error: Some(json!({ "code": -32700, "message": "Parse error" })),
52 }),
53 }
54 }
55 Json(json!(responses))
56 }
57 single => match serde_json::from_value::<RpcRequest>(single) {
58 Ok(req) => Json(json!(dispatch(&state, req).await)),
59 Err(_) => Json(json!(RpcResponse {
60 jsonrpc: "2.0", result: None, id: Value::Null,
61 error: Some(json!({ "code": -32700, "message": "Parse error" })),
62 })),
63 },
64 }
65}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Modeling the wire protocol as typed structs lets serde enforce the shape while you focus on dispatch logic.
- 2JSON-RPC's spec-defined error codes give clients a stable contract even when parsing or method lookup fails.
- 3Handling batch and single requests through one shared dispatch path keeps behavior consistent across both forms.
Related explainers
rust
use axum::{extract::{Path, State}, http::StatusCode, Json}; use dashmap::DashMap; use serde::Serialize; use std::sync::Arc;
Request coalescing in an Axum handler
caching
concurrency
request-coalescing
Advanced
8 steps
ruby
class TemplateInterpolator PLACEHOLDER = /\{\{\s*([\w.]+)\s*\}\}/ def initialize(strict: false)
Interpolating templates with dotted keys in Ruby
regex
string-interpolation
hash-traversal
Intermediate
6 steps
go
package config import ( "fmt"
A thread-safe config singleton in Go
singleton
concurrency
environment-variables
Intermediate
7 steps
rust
use std::net::Ipv4Addr; use std::str::FromStr; #[derive(Debug, Clone, Copy)]
Parsing and matching IPv4 CIDR ranges in Rust
bitwise-operations
parsing
error-handling
Intermediate
8 steps
rust
use std::sync::OnceLock; static CRC_TABLE: OnceLock<[u32; 256]> = OnceLock::new();
Table-driven CRC32 with lazy init in Rust
checksums
lazy-initialization
lookup-tables
Intermediate
8 steps
typescript
import { parsePhoneNumberFromString, CountryCode } from 'libphonenumber-js'; export interface NormalizedPhone { e164: string;
Normalizing phone numbers to E.164 in TypeScript
validation
normalization
error-handling
Intermediate
7 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/building-a-json-rpc-2-0-handler-in-axum-explained-rust-bf56/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.