# Send JSON-RPC Requests

## Overview

[`RpcRequest`](/api/RpcRequest) builds [JSON-RPC 2.0](https://www.jsonrpc.org/specification)
request objects, [`RpcResponse`](/api/RpcResponse) parses what the node returns, and
[`RpcTransport`](/api/RpcTransport) bundles both behind an HTTP `request` function. All three are
stateless and transport-agnostic — pair them with `fetch`, WebSockets, or any messaging layer.

## Recipes

### Build a Request Store

[`RpcRequest.createStore`](/api/RpcRequest/createStore) returns a `prepare` function that builds
strongly-typed JSON-RPC request objects with an auto-incrementing `id`.

```ts twoslash
import { RpcRequest } from 'ox'

const store = RpcRequest.createStore()

const request_1 = store.prepare({
  method: 'eth_blockNumber',
})
// @log: { id: 0, jsonrpc: '2.0', method: 'eth_blockNumber' }

const request_2 = store.prepare({
  method: 'eth_getBlockByNumber',
  params: ['latest', false],
})
// @log: { id: 1, jsonrpc: '2.0', method: 'eth_getBlockByNumber', params: ['latest', false] }
```

Use [`RpcRequest.from`](/api/RpcRequest/from) instead to build a single request and manage the
`id` yourself.

### Send over HTTP Fetch

A prepared request is a plain JSON-serializable object — POST it to any RPC endpoint.

```ts twoslash
import { RpcRequest } from 'ox'

const store = RpcRequest.createStore()

const request = store.prepare({
  method: 'eth_getBlockByNumber',
  params: ['latest', false],
})

const response = await fetch('https://1.rpc.thirdweb.com', {
  body: JSON.stringify(request),
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
}).then((res) => res.json())
```

### Send with an RPC Transport

[`RpcTransport.fromHttp`](/api/RpcTransport/fromHttp) wraps the prepare → fetch → parse loop into
a single `request` function that manages `id`s and raises JSON-RPC errors for you.

```ts twoslash
import { RpcTransport } from 'ox'

const transport = RpcTransport.fromHttp('https://1.rpc.thirdweb.com')

const blockNumber = await transport.request({ method: 'eth_blockNumber' })
// @log: '0x1a2b3c'
```

The transport is also EIP-1193 compatible — see
[Use EIP-1193 Providers](/guides/rpc/providers).

### Parse Responses

[`RpcResponse.parse`](/api/RpcResponse/parse) extracts the JSON-RPC `result`, throws a typed error
when the response contains an `error`, and — given the originating `request` — strongly types the
result.

```ts twoslash
import { RpcRequest, RpcResponse } from 'ox'

const store = RpcRequest.createStore()

const request = store.prepare({
  method: 'eth_getBlockByNumber',
  params: ['latest', false],
})

const block = await fetch('https://1.rpc.thirdweb.com', {
  body: JSON.stringify(request),
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
})
  .then((res) => res.json())
  .then((res) => RpcResponse.parse(res, { request }))
```

Set `raw: true` to receive the whole `{ result, error }` response object instead of throwing on
errors.

## Best Practices

### Reuse One Store per Connection

A store increments its `id` on every `prepare` call. Create one store per connection or session so
ids stay unique and responses can be correlated with their requests.

### Let a Transport Own the Plumbing

Reach for `RpcTransport.fromHttp` unless you control the wire format yourself. For a full-featured
client with retries, batching, and wallet actions, use [Viem](https://viem.sh).

## See More

<Cards>
  <Card icon="lucide:server" title="Serve & Handle RPC Requests" description="Answer incoming JSON-RPC requests and return typed errors." to="/guides/rpc/handling" />

  <Card icon="lucide:braces" title="Type-Safe RPC Schemas" description="Statically type custom methods across stores, transports, and providers." to="/guides/rpc/schemas" />

  <Card icon="lucide:plug" title="Use EIP-1193 Providers" description="Wrap injected providers or turn a transport into a provider." to="/guides/rpc/providers" />
</Cards>
