Documentation Index
Fetch the complete documentation index at: https://docs.go-mizu.dev/llms.txt
Use this file to discover all available pages before exploring further.
What is JSON-RPC?
JSON-RPC (Remote Procedure Call) is a protocol where you explicitly name the method you want to call. Unlike REST which uses different HTTP verbs and paths, JSON-RPC sends all requests to a single endpoint with the method name in the request body.
Its killer feature? Batching - send multiple requests in one HTTP call.
{
"jsonrpc": "2.0",
"id": 1,
"method": "todos.create",
"params": {"title": "Buy milk"}
}
Quick Start
import (
"github.com/go-mizu/mizu"
contract "github.com/go-mizu/mizu/contract/v2"
"github.com/go-mizu/mizu/contract/v2/transport/jsonrpc"
"yourapp/todo"
)
// Your interface is defined in the todo package as todo.API:
// type API interface {
// Create(ctx context.Context, in *CreateInput) (*Todo, error)
// List(ctx context.Context) (*ListOutput, error)
// Get(ctx context.Context, in *GetInput) (*Todo, error)
// }
// Create your service implementation
impl := todo.NewService()
// Register your service
svc := contract.Register[todo.API](impl,
contract.WithDefaultResource("todos"),
)
// Create mizu app
app := mizu.New()
// Mount JSON-RPC at /rpc
jsonrpc.Mount(app.Router, "/rpc", svc)
// Start server
app.Listen(":8080")
Now call methods:
curl -X POST http://localhost:8080/rpc \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "todos.create",
"params": {"title": "Buy milk"}
}'
Every JSON-RPC request has the same structure:
{
"jsonrpc": "2.0",
"id": 1,
"method": "todos.create",
"params": {"title": "Buy milk"}
}
| Field | Required | Description |
|---|
jsonrpc | Yes | Always "2.0" |
id | No* | Identifies your request (returned in response) |
method | Yes | resource.method name to call |
params | No | Object with method parameters |
*If you omit id, it becomes a “notification” (no response sent).
Success Response
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"id": "todo_1",
"title": "Buy milk",
"completed": false
}
}
Error Response
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32603,
"message": "todo not found",
"data": {"todoId": "123"}
}
}
Method Naming
Method names follow the pattern resource.method:
| Interface Method | JSON-RPC Method |
|---|
Create | todos.create |
List | todos.list |
Get | todos.get |
Delete | todos.delete |
Update | todos.update |
The resource name comes from WithDefaultResource or WithResource.
Batching
Batching lets you send multiple requests in one HTTP call. Instead of N HTTP round trips, you make just 1.
How to Batch
Send an array of requests:
curl -X POST http://localhost:8080/rpc \
-H "Content-Type: application/json" \
-d '[
{"jsonrpc": "2.0", "id": 1, "method": "todos.create", "params": {"title": "First"}},
{"jsonrpc": "2.0", "id": 2, "method": "todos.create", "params": {"title": "Second"}},
{"jsonrpc": "2.0", "id": 3, "method": "todos.list"}
]'
Response:
[
{"jsonrpc": "2.0", "id": 1, "result": {"id": "1", "title": "First", "completed": false}},
{"jsonrpc": "2.0", "id": 2, "result": {"id": "2", "title": "Second", "completed": false}},
{"jsonrpc": "2.0", "id": 3, "result": {"items": [...], "count": 2}}
]
When to Use Batching
- Dashboard loads: Fetch user, settings, and recent items in one call
- Bulk operations: Create 100 items without 100 HTTP requests
- Related data: Get a todo and its comments together
- Service-to-service: Microservices calling each other
Notifications (Fire and Forget)
Omit the id field to send a notification. The server won’t send a response:
curl -X POST http://localhost:8080/rpc \
-H "Content-Type: application/json" \
-d '{"jsonrpc": "2.0", "method": "todos.create", "params": {"title": "Fire and forget"}}'
# HTTP 204 No Content
Use notifications for:
- Logging or analytics events
- Triggering background jobs
- Sending metrics
You can batch notifications:
curl -X POST http://localhost:8080/rpc \
-d '[
{"jsonrpc": "2.0", "method": "analytics.log", "params": {"event": "page_view"}},
{"jsonrpc": "2.0", "method": "analytics.log", "params": {"event": "click"}}
]'
# HTTP 204 No Content
Complete Example
This example shows a JSON-RPC service using the recommended package-based organization:
// todo/api.go
package todo
import "context"
// API defines the contract for todo operations
type API interface {
Create(ctx context.Context, in *CreateInput) (*Todo, error)
List(ctx context.Context) (*ListOutput, error)
Get(ctx context.Context, in *GetInput) (*Todo, error)
}
// todo/service.go
package todo
import (
"context"
"fmt"
"sync"
contract "github.com/go-mizu/mizu/contract/v2"
)
// Service implements todo.API
type Service struct {
mu sync.RWMutex
todos map[string]*Todo
nextID int
}
var _ API = (*Service)(nil)
func NewService() *Service {
return &Service{todos: make(map[string]*Todo)}
}
func (s *Service) Create(ctx context.Context, in *CreateInput) (*Todo, error) {
if in.Title == "" {
return nil, contract.ErrInvalidArgument("title is required")
}
s.mu.Lock()
defer s.mu.Unlock()
s.nextID++
todo := &Todo{
ID: fmt.Sprintf("%d", s.nextID),
Title: in.Title,
}
s.todos[todo.ID] = todo
return todo, nil
}
func (s *Service) List(ctx context.Context) (*ListOutput, error) {
s.mu.RLock()
defer s.mu.RUnlock()
items := make([]*Todo, 0, len(s.todos))
for _, t := range s.todos {
items = append(items, t)
}
return &ListOutput{Items: items, Count: len(items)}, nil
}
func (s *Service) Get(ctx context.Context, in *GetInput) (*Todo, error) {
s.mu.RLock()
defer s.mu.RUnlock()
todo, ok := s.todos[in.ID]
if !ok {
return nil, contract.ErrNotFound("todo not found")
}
return todo, nil
}
// main.go
package main
import (
"fmt"
"github.com/go-mizu/mizu"
contract "github.com/go-mizu/mizu/contract/v2"
"github.com/go-mizu/mizu/contract/v2/transport/jsonrpc"
"yourapp/todo"
)
func main() {
impl := todo.NewService()
svc := contract.Register[todo.API](impl,
contract.WithDefaultResource("todos"),
)
app := mizu.New()
// Mount JSON-RPC at /rpc
jsonrpc.Mount(app.Router, "/rpc", svc)
fmt.Println("JSON-RPC server running at http://localhost:8080/rpc")
fmt.Println("Methods: todos.create, todos.list, todos.get")
app.Listen(":8080")
}
Test It
# Create a todo
curl -X POST http://localhost:8080/rpc \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"todos.create","params":{"title":"Buy milk"}}'
# List all todos
curl -X POST http://localhost:8080/rpc \
-d '{"jsonrpc":"2.0","id":2,"method":"todos.list"}'
# Get a todo
curl -X POST http://localhost:8080/rpc \
-d '{"jsonrpc":"2.0","id":3,"method":"todos.get","params":{"id":"1"}}'
# Batch request
curl -X POST http://localhost:8080/rpc \
-d '[
{"jsonrpc":"2.0","id":1,"method":"todos.create","params":{"title":"First"}},
{"jsonrpc":"2.0","id":2,"method":"todos.create","params":{"title":"Second"}},
{"jsonrpc":"2.0","id":3,"method":"todos.list"}
]'
Error Codes
JSON-RPC uses standard error codes:
| Code | Message | What It Means |
|---|
| -32700 | Parse error | Invalid JSON |
| -32600 | Invalid Request | Missing jsonrpc or method |
| -32601 | Method not found | Method doesn’t exist |
| -32602 | Invalid params | Wrong parameter types |
| -32603 | Internal error | Your method returned an error |
Contract errors map to -32603 (Internal error) with the message from your error:
// In your method
return nil, contract.ErrNotFound("todo not found")
// JSON-RPC response
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32603,
"message": "todo not found",
"data": {"code": "NOT_FOUND"}
}
}
Multiple Services
Mount multiple services on the same endpoint:
todoSvc := contract.Register[todo.API](todoImpl, contract.WithDefaultResource("todos"))
userSvc := contract.Register[user.API](userImpl, contract.WithDefaultResource("users"))
app := mizu.New()
// Both mounted at /rpc
jsonrpc.Mount(app.Router, "/rpc", todoSvc)
jsonrpc.Mount(app.Router, "/rpc", userSvc)
// Now call:
// {"method": "todos.create", ...}
// {"method": "users.create", ...}
Combining with REST
Use JSON-RPC alongside REST:
import (
"github.com/go-mizu/mizu/contract/v2/transport/rest"
"github.com/go-mizu/mizu/contract/v2/transport/jsonrpc"
)
app := mizu.New()
// REST for browsers and simple clients
rest.Mount(app.Router, svc)
// JSON-RPC for batching and service-to-service
jsonrpc.Mount(app.Router, "/rpc", svc)
app.Listen(":8080")
JSON-RPC vs REST
| Use JSON-RPC when… | Use REST when… |
|---|
| You need batching | Browser is the main client |
| Service-to-service calls | You want HTTP caching |
| Methods don’t map to CRUD | Team knows REST |
| You want explicit method names | Building a public API |
Common Questions
Can I mix requests and notifications in a batch?
Yes! Requests (with id) get responses, notifications (without id) don’t:
[
{"jsonrpc": "2.0", "id": 1, "method": "todos.get", "params": {"id": "1"}},
{"jsonrpc": "2.0", "method": "analytics.log", "params": {"event": "viewed"}}
]
Response only includes the request:
[
{"jsonrpc": "2.0", "id": 1, "result": {...}}
]
What ID values can I use?
Any JSON value works:
{"id": 1, ...}
{"id": "req-abc-123", ...}
{"id": null, ...}
How do I call methods without a resource?
If you didn’t use WithDefaultResource, use just the method name:
{"method": "Create", ...}
With a resource:
{"method": "todos.create", ...}
Can I generate an OpenRPC spec?
Yes:
spec, err := jsonrpc.OpenRPC(svc.Descriptor())
What’s Next?