oRPC Extension
JSON-RPC 2.0 server with OpenRPC schema and automatic REST-to-RPC conversion
oRPC Extension
The oRPC (Open RPC) extension automatically exposes your Forge application's REST API as JSON-RPC 2.0 methods with OpenRPC schema support. This provides a unified RPC interface for your existing HTTP endpoints with full specification compliance.
Features
JSON-RPC 2.0 Compliance
- Full Specification Support: 100% compliant with JSON-RPC 2.0 specification
- Request/Response Handling: Proper request validation and response formatting
- Batch Requests: Support for batch request processing with configurable limits
- Notification Support: Handle notification requests (requests without ID)
- Error Handling: Standard JSON-RPC error codes and custom error responses
OpenRPC Schema Generation
- Automatic Schema: Generates OpenRPC 1.3.2 compliant schema documentation
- Type Safety: Schema validation and type hints for parameters and results
- Method Discovery: Runtime method discovery and introspection
- Custom Annotations: Add custom parameter and result schemas via route options
- Documentation: Rich method descriptions and examples
REST-to-RPC Conversion
- Automatic Method Generation: Converts REST routes to JSON-RPC 2.0 methods
- Route Execution: Executes underlying HTTP endpoints via JSON-RPC calls
- Flexible Naming: Multiple naming strategies (path-based, method-based, custom)
- Pattern Matching: Include/exclude routes with glob patterns
- Custom Method Names: Override auto-generated method names
Advanced Features
- Interceptors: Add middleware for authentication, logging, and metrics
- Rate Limiting: Built-in rate limiting with configurable limits
- Request Size Limits: Prevent memory exhaustion with size controls
- Caching: Schema caching for improved performance
- Observability: Integrated metrics, logging, and tracing
Installation
Go Module
go get github.com/xraph/forge/extensions/orpcDocker
FROM xraph/forge:latest
# oRPC extension is includedPackage Manager
# Using Forge CLI
forge extension add orpc
# Using package manager
npm install @xraph/forge-orpcConfiguration
YAML Configuration
extensions:
orpc:
# Core settings
enabled: true
endpoint: "/rpc"
openrpc_endpoint: "/rpc/schema"
# Server information
server_name: "My API"
server_version: "1.0.0"
# Auto-exposure settings
auto_expose_routes: true
method_prefix: "api."
exclude_patterns:
- "/_/*" # Exclude health/metrics endpoints
- "/internal/*" # Exclude internal routes
- "/debug/*" # Exclude debug routes
include_patterns: [] # If set, only expose matching patterns
# Features
enable_openrpc: true
enable_discovery: true
enable_batch: true
batch_limit: 10
# Naming strategy: "path", "method", or "custom"
naming_strategy: "path"
# Security & Performance
max_request_size: 1048576 # 1MB
request_timeout: 30 # seconds
schema_cache: true
enable_metrics: true
# Authentication (optional)
auth:
header: "X-API-Key"
tokens:
- "secret-token-1"
- "secret-token-2"
# Rate limiting (optional)
rate_limit:
requests_per_minute: 100
burst: 10Environment Variables
# Core Configuration
FORGE_ORPC_ENABLED=true
FORGE_ORPC_ENDPOINT=/rpc
FORGE_ORPC_OPENRPC_ENDPOINT=/rpc/schema
# Server Information
FORGE_ORPC_SERVER_NAME="My API"
FORGE_ORPC_SERVER_VERSION=1.0.0
# Auto-exposure
FORGE_ORPC_AUTO_EXPOSE_ROUTES=true
FORGE_ORPC_METHOD_PREFIX=api.
FORGE_ORPC_EXCLUDE_PATTERNS="/_/*,/internal/*"
# Features
FORGE_ORPC_ENABLE_OPENRPC=true
FORGE_ORPC_ENABLE_DISCOVERY=true
FORGE_ORPC_ENABLE_BATCH=true
FORGE_ORPC_BATCH_LIMIT=10
# Performance
FORGE_ORPC_MAX_REQUEST_SIZE=1048576
FORGE_ORPC_REQUEST_TIMEOUT=30
FORGE_ORPC_SCHEMA_CACHE=trueProgrammatic Configuration
package main
import (
"github.com/xraph/forge"
"github.com/xraph/forge/extensions/orpc"
)
func main() {
app := forge.NewApp(forge.AppConfig{
Name: "my-api",
Version: "1.0.0",
})
// Configure oRPC extension
app.RegisterExtension(orpc.NewExtension(
orpc.WithEnabled(true),
orpc.WithEndpoint("/rpc"),
orpc.WithOpenRPCEndpoint("/rpc/schema"),
orpc.WithServerInfo("My API", "1.0.0"),
orpc.WithAutoExposeRoutes(true),
orpc.WithMethodPrefix("api."),
orpc.WithExcludePatterns([]string{"/_/*", "/internal/*"}),
orpc.WithIncludePatterns([]string{"/api/*"}), // Only expose /api/* routes
orpc.WithOpenRPC(true),
orpc.WithDiscovery(true),
orpc.WithBatch(true),
orpc.WithBatchLimit(10),
orpc.WithNamingStrategy("path"),
orpc.WithMaxRequestSize(1024 * 1024), // 1MB
orpc.WithRequestTimeout(30), // seconds
orpc.WithSchemaCache(true),
orpc.WithMetrics(true),
))
app.Start()
}Usage Examples
Basic REST API with Auto-Exposure
package main
import (
"github.com/xraph/forge"
"github.com/xraph/forge/extensions/orpc"
)
type User struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
type CreateUserRequest struct {
Name string `json:"name" validate:"required"`
Email string `json:"email" validate:"required,email"`
}
func main() {
app := forge.NewApp(forge.AppConfig{
Name: "user-api",
Version: "1.0.0",
})
// Add REST routes
app.Router().GET("/users/:id", getUserHandler,
forge.WithSummary("Get user by ID"),
forge.WithDescription("Retrieve a user by their unique identifier"),
forge.WithTags("users"),
forge.WithORPCMethod("user.get"), // Custom JSON-RPC method name
forge.WithORPCParams(&orpc.ParamsSchema{
Type: "object",
Properties: map[string]*orpc.PropertySchema{
"id": {
Type: "string",
Description: "User ID",
Example: "user-123",
},
},
Required: []string{"id"},
}),
forge.WithORPCResult(&orpc.ResultSchema{
Type: "object",
Description: "User object",
Properties: map[string]*orpc.PropertySchema{
"id": {Type: "string", Description: "User ID"},
"name": {Type: "string", Description: "User name"},
"email": {Type: "string", Description: "User email"},
},
}),
)
app.Router().POST("/users", createUserHandler,
forge.WithSummary("Create new user"),
forge.WithDescription("Create a new user account"),
forge.WithTags("users"),
forge.WithORPCMethod("user.create"),
forge.WithRequestBody(CreateUserRequest{}, "User creation data"),
forge.WithResponse(201, User{}, "Created user"),
)
app.Router().PUT("/users/:id", updateUserHandler,
forge.WithSummary("Update user"),
forge.WithTags("users"),
forge.WithORPCMethod("user.update"),
)
app.Router().DELETE("/users/:id", deleteUserHandler,
forge.WithSummary("Delete user"),
forge.WithTags("users"),
forge.WithORPCMethod("user.delete"),
)
// Exclude internal routes from oRPC exposure
app.Router().GET("/_/health", healthHandler,
forge.WithORPCExclude(), // Don't expose as JSON-RPC method
)
// Enable oRPC with auto-exposure
app.RegisterExtension(orpc.NewExtension(
orpc.WithEnabled(true),
orpc.WithAutoExposeRoutes(true),
orpc.WithMethodPrefix("api."),
orpc.WithExcludePatterns([]string{"/_/*"}),
))
app.Start()
}
func getUserHandler(ctx forge.Context) error {
userID := ctx.Param("id")
// Simulate user lookup
user := User{
ID: userID,
Name: "John Doe",
Email: "john@example.com",
}
return ctx.JSON(200, user)
}
func createUserHandler(ctx forge.Context) error {
var req CreateUserRequest
if err := ctx.Bind(&req); err != nil {
return ctx.JSON(400, map[string]string{"error": "Invalid request"})
}
// Simulate user creation
user := User{
ID: "user-" + generateID(),
Name: req.Name,
Email: req.Email,
}
return ctx.JSON(201, user)
}
func updateUserHandler(ctx forge.Context) error {
userID := ctx.Param("id")
var req CreateUserRequest
if err := ctx.Bind(&req); err != nil {
return ctx.JSON(400, map[string]string{"error": "Invalid request"})
}
// Simulate user update
user := User{
ID: userID,
Name: req.Name,
Email: req.Email,
}
return ctx.JSON(200, user)
}
func deleteUserHandler(ctx forge.Context) error {
userID := ctx.Param("id")
// Simulate user deletion
return ctx.JSON(200, map[string]string{
"message": "User deleted",
"id": userID,
})
}This automatically creates JSON-RPC methods:
api.user.get→GET /users/:idapi.user.create→POST /usersapi.user.update→PUT /users/:idapi.user.delete→DELETE /users/:id
Manual Method Registration
package main
import (
"context"
"github.com/xraph/forge"
"github.com/xraph/forge/extensions/orpc"
)
func main() {
app := forge.NewApp(forge.AppConfig{
Name: "calculator-api",
Version: "1.0.0",
})
// Register oRPC extension
app.RegisterExtension(orpc.NewExtension(
orpc.WithEnabled(true),
orpc.WithAutoExposeRoutes(false), // Disable auto-exposure
))
app.Start()
// Get oRPC server from DI container
orpcServer := forge.Must[orpc.ORPC](app.Container(), "orpc")
// Register custom methods
orpcServer.RegisterMethod(&orpc.Method{
Name: "math.add",
Description: "Add two numbers",
Params: &orpc.ParamsSchema{
Type: "object",
Properties: map[string]*orpc.PropertySchema{
"a": {Type: "number", Description: "First number"},
"b": {Type: "number", Description: "Second number"},
},
Required: []string{"a", "b"},
},
Result: &orpc.ResultSchema{
Type: "number",
Description: "Sum of a and b",
},
Handler: func(ctx context.Context, params json.RawMessage) (interface{}, error) {
var args struct {
A float64 `json:"a"`
B float64 `json:"b"`
}
if err := json.Unmarshal(params, &args); err != nil {
return nil, orpc.NewError(orpc.ErrInvalidParams, "Invalid parameters", nil)
}
return args.A + args.B, nil
},
Tags: []string{"math", "arithmetic"},
})
orpcServer.RegisterMethod(&orpc.Method{
Name: "math.multiply",
Description: "Multiply two numbers",
Params: &orpc.ParamsSchema{
Type: "object",
Properties: map[string]*orpc.PropertySchema{
"a": {Type: "number", Description: "First number"},
"b": {Type: "number", Description: "Second number"},
},
Required: []string{"a", "b"},
},
Result: &orpc.ResultSchema{
Type: "number",
Description: "Product of a and b",
},
Handler: func(ctx context.Context, params json.RawMessage) (interface{}, error) {
var args struct {
A float64 `json:"a"`
B float64 `json:"b"`
}
if err := json.Unmarshal(params, &args); err != nil {
return nil, orpc.NewError(orpc.ErrInvalidParams, "Invalid parameters", nil)
}
return args.A * args.B, nil
},
Tags: []string{"math", "arithmetic"},
})
app.Run()
}Client Usage Examples
Single Request
# Using curl
curl -X POST http://localhost:8080/rpc \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"method": "api.user.get",
"params": {"id": "user-123"},
"id": 1
}'
# Response
{
"jsonrpc": "2.0",
"result": {
"id": "user-123",
"name": "John Doe",
"email": "john@example.com"
},
"id": 1
}Batch Request
curl -X POST http://localhost:8080/rpc \
-H "Content-Type: application/json" \
-d '[
{
"jsonrpc": "2.0",
"method": "api.user.get",
"params": {"id": "user-123"},
"id": 1
},
{
"jsonrpc": "2.0",
"method": "api.user.create",
"params": {
"name": "Jane Doe",
"email": "jane@example.com"
},
"id": 2
}
]'
# Response
[
{
"jsonrpc": "2.0",
"result": {
"id": "user-123",
"name": "John Doe",
"email": "john@example.com"
},
"id": 1
},
{
"jsonrpc": "2.0",
"result": {
"id": "user-456",
"name": "Jane Doe",
"email": "jane@example.com"
},
"id": 2
}
]Notification (No Response)
curl -X POST http://localhost:8080/rpc \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"method": "api.user.delete",
"params": {"id": "user-123"}
}'
# No response (notification)JavaScript Client
class ORPCClient {
constructor(endpoint) {
this.endpoint = endpoint;
this.id = 1;
}
async call(method, params) {
const request = {
jsonrpc: "2.0",
method: method,
params: params,
id: this.id++
};
const response = await fetch(this.endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(request)
});
const result = await response.json();
if (result.error) {
throw new Error(`RPC Error ${result.error.code}: ${result.error.message}`);
}
return result.result;
}
async batch(calls) {
const requests = calls.map(([method, params]) => ({
jsonrpc: "2.0",
method: method,
params: params,
id: this.id++
}));
const response = await fetch(this.endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requests)
});
return await response.json();
}
notify(method, params) {
const request = {
jsonrpc: "2.0",
method: method,
params: params
};
return fetch(this.endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(request)
});
}
}
// Usage
const client = new ORPCClient('http://localhost:8080/rpc');
// Single call
const user = await client.call('api.user.get', { id: 'user-123' });
console.log(user);
// Batch call
const results = await client.batch([
['api.user.get', { id: 'user-123' }],
['api.user.create', { name: 'Jane Doe', email: 'jane@example.com' }]
]);
console.log(results);
// Notification
client.notify('api.user.delete', { id: 'user-123' });Advanced Features
Custom Interceptors
package main
import (
"context"
"strings"
"time"
"github.com/xraph/forge"
"github.com/xraph/forge/extensions/orpc"
)
// AuthInterceptor validates API keys
func AuthInterceptor(validTokens []string) orpc.Interceptor {
return func(ctx context.Context, req *orpc.Request, next orpc.Handler) *orpc.Response {
// Skip auth for discovery methods
if strings.HasPrefix(req.Method, "rpc.") {
return next(ctx, req)
}
// Check for API key in context (from HTTP header)
apiKey := ctx.Value("api_key")
if apiKey == nil {
return orpc.NewErrorResponse(req.ID, orpc.ErrUnauthorized, "Missing API key", nil)
}
// Validate API key
valid := false
for _, token := range validTokens {
if apiKey == token {
valid = true
break
}
}
if !valid {
return orpc.NewErrorResponse(req.ID, orpc.ErrUnauthorized, "Invalid API key", nil)
}
return next(ctx, req)
}
}
// LoggingInterceptor logs all RPC calls
func LoggingInterceptor(logger forge.Logger) orpc.Interceptor {
return func(ctx context.Context, req *orpc.Request, next orpc.Handler) *orpc.Response {
start := time.Now()
logger.Info("rpc call started",
forge.F("method", req.Method),
forge.F("id", req.ID),
forge.F("has_params", req.Params != nil),
)
resp := next(ctx, req)
duration := time.Since(start)
if resp.Error != nil {
logger.Error("rpc call failed",
forge.F("method", req.Method),
forge.F("id", req.ID),
forge.F("error_code", resp.Error.Code),
forge.F("error_message", resp.Error.Message),
forge.F("duration", duration),
)
} else {
logger.Info("rpc call completed",
forge.F("method", req.Method),
forge.F("id", req.ID),
forge.F("duration", duration),
)
}
return resp
}
}
// MetricsInterceptor records metrics
func MetricsInterceptor(metrics forge.Metrics) orpc.Interceptor {
return func(ctx context.Context, req *orpc.Request, next orpc.Handler) *orpc.Response {
start := time.Now()
metrics.Counter("orpc_requests_total").
WithLabels(forge.Labels{"method": req.Method}).
Inc()
resp := next(ctx, req)
duration := time.Since(start)
status := "success"
if resp.Error != nil {
status = "error"
}
metrics.Counter("orpc_requests_completed_total").
WithLabels(forge.Labels{
"method": req.Method,
"status": status,
}).
Inc()
metrics.Histogram("orpc_request_duration_seconds").
WithLabels(forge.Labels{"method": req.Method}).
Observe(duration.Seconds())
return resp
}
}
func main() {
app := forge.NewApp(forge.AppConfig{
Name: "secure-api",
Version: "1.0.0",
})
// Register oRPC extension
app.RegisterExtension(orpc.NewExtension(
orpc.WithEnabled(true),
orpc.WithAutoExposeRoutes(true),
))
app.Start()
// Get oRPC server and add interceptors
orpcServer := forge.Must[orpc.ORPC](app.Container(), "orpc")
logger := forge.Must[forge.Logger](app.Container(), "logger")
metrics := forge.Must[forge.Metrics](app.Container(), "metrics")
// Add interceptors (order matters - they wrap each other)
orpcServer.Use(LoggingInterceptor(logger))
orpcServer.Use(MetricsInterceptor(metrics))
orpcServer.Use(AuthInterceptor([]string{"secret-key-1", "secret-key-2"}))
app.Run()
}Schema Validation
package main
import (
"context"
"encoding/json"
"fmt"
"github.com/xraph/forge"
"github.com/xraph/forge/extensions/orpc"
"github.com/go-playground/validator/v10"
)
type UserService struct {
validator *validator.Validate
}
func NewUserService() *UserService {
return &UserService{
validator: validator.New(),
}
}
func (s *UserService) RegisterMethods(orpcServer orpc.ORPC) {
// Register user.create method with validation
orpcServer.RegisterMethod(&orpc.Method{
Name: "user.create",
Description: "Create a new user with validation",
Params: &orpc.ParamsSchema{
Type: "object",
Properties: map[string]*orpc.PropertySchema{
"name": {
Type: "string",
Description: "User's full name",
MinLength: &[]int{2}[0],
MaxLength: &[]int{100}[0],
},
"email": {
Type: "string",
Description: "User's email address",
Format: "email",
},
"age": {
Type: "integer",
Description: "User's age",
Minimum: &[]float64{18}[0],
Maximum: &[]float64{120}[0],
},
},
Required: []string{"name", "email", "age"},
},
Result: &orpc.ResultSchema{
Type: "object",
Description: "Created user",
Properties: map[string]*orpc.PropertySchema{
"id": {Type: "string", Description: "User ID"},
"name": {Type: "string", Description: "User name"},
"email": {Type: "string", Description: "User email"},
"age": {Type: "integer", Description: "User age"},
},
},
Handler: s.createUser,
Tags: []string{"users", "crud"},
})
}
func (s *UserService) createUser(ctx context.Context, params json.RawMessage) (interface{}, error) {
var req struct {
Name string `json:"name" validate:"required,min=2,max=100"`
Email string `json:"email" validate:"required,email"`
Age int `json:"age" validate:"required,min=18,max=120"`
}
// Parse parameters
if err := json.Unmarshal(params, &req); err != nil {
return nil, orpc.NewError(orpc.ErrInvalidParams, "Invalid JSON parameters", nil)
}
// Validate parameters
if err := s.validator.Struct(req); err != nil {
var validationErrors []string
for _, err := range err.(validator.ValidationErrors) {
validationErrors = append(validationErrors, fmt.Sprintf(
"Field '%s' failed validation: %s",
err.Field(),
err.Tag(),
))
}
return nil, orpc.NewError(orpc.ErrInvalidParams, "Validation failed", map[string]interface{}{
"errors": validationErrors,
})
}
// Simulate user creation
user := map[string]interface{}{
"id": fmt.Sprintf("user-%d", time.Now().Unix()),
"name": req.Name,
"email": req.Email,
"age": req.Age,
}
return user, nil
}OpenRPC Schema Access
# Get OpenRPC schema
curl http://localhost:8080/rpc/schema
# Response
{
"openrpc": "1.3.2",
"info": {
"title": "My API",
"version": "1.0.0",
"description": "JSON-RPC 2.0 API"
},
"methods": [
{
"name": "api.user.get",
"description": "Get user by ID",
"params": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "User ID"
}
},
"required": ["id"]
},
"result": {
"type": "object",
"description": "User object",
"properties": {
"id": {"type": "string"},
"name": {"type": "string"},
"email": {"type": "string"}
}
},
"tags": ["users"]
}
]
}
# List available methods
curl http://localhost:8080/rpc/methods
# Response
{
"methods": [
"api.user.get",
"api.user.create",
"api.user.update",
"api.user.delete"
]
}Best Practices
Method Design
- Use semantic naming: Choose descriptive method names that clearly indicate functionality
- Follow conventions: Use dot notation for namespacing (e.g.,
user.create,order.list) - Version your methods: Include version in method names for breaking changes
- Document thoroughly: Provide clear descriptions and examples
Parameter Validation
- Validate all inputs: Use schema validation and custom validators
- Provide clear errors: Return meaningful error messages with validation details
- Use appropriate types: Choose correct JSON schema types for parameters
- Set reasonable limits: Define min/max values and string lengths
Error Handling
- Use standard codes: Follow JSON-RPC 2.0 error code conventions
- Provide context: Include helpful error details and suggestions
- Log errors properly: Log errors with sufficient context for debugging
- Handle edge cases: Consider null values, empty arrays, and invalid types
Performance Optimization
- Enable caching: Use schema caching for better performance
- Set request limits: Configure appropriate request size and batch limits
- Use interceptors wisely: Avoid heavy processing in interceptors
- Monitor metrics: Track request rates, durations, and error rates
Security Considerations
- Implement authentication: Validate API keys or tokens
- Use rate limiting: Prevent abuse with rate limiting
- Validate input size: Set maximum request sizes
- Sanitize parameters: Validate and sanitize all input data
Troubleshooting
Common Issues
Method Not Found
{
"jsonrpc": "2.0",
"error": {
"code": -32601,
"message": "Method not found"
},
"id": 1
}Solutions:
- Check method name spelling
- Verify auto-exposure patterns
- Ensure route is registered before oRPC starts
- Check exclude/include patterns
Invalid Parameters
{
"jsonrpc": "2.0",
"error": {
"code": -32602,
"message": "Invalid params",
"data": {
"errors": ["Field 'email' failed validation: email"]
}
},
"id": 1
}Solutions:
- Validate parameter schema
- Check required fields
- Verify parameter types
- Review validation rules
Batch Too Large
{
"jsonrpc": "2.0",
"error": {
"code": -32000,
"message": "Batch size exceeds limit"
},
"id": null
}Solutions:
- Reduce batch size
- Increase
batch_limitconfiguration - Split large batches into smaller ones
Debugging
Enable Debug Logging
logging:
level: debug
loggers:
orpc: debug
orpc.server: debug
orpc.schema: debugMonitor Metrics
// Check server statistics
stats := orpcServer.GetStats()
fmt.Printf("Total Requests: %d\n", stats.TotalRequests)
fmt.Printf("Total Errors: %d\n", stats.TotalErrors)
fmt.Printf("Average Duration: %v\n", stats.AverageDuration)Inspect Schema
# Get schema for debugging
curl http://localhost:8080/rpc/schema | jq .
# List all methods
curl http://localhost:8080/rpc/methods | jq .Next Steps
- Explore gRPC Extension for high-performance binary RPC
- Learn about Streaming Extension for real-time data streaming
- Check out WebRTC Extension for peer-to-peer communication
- Review GraphQL Extension for flexible query APIs
- See Events Extension for event-driven architecture
- Visit Auth Extension for authentication integration
How is this guide?
Last updated on