Skip to content
/ zrpc Public

A modern RPC framework for Elixir with a clean DSL, middleware system, and hierarchical routing. Define your API once, generate TypeScript clients and OpenAPI specs automatically.

License

Notifications You must be signed in to change notification settings

wavezync/zrpc

Repository files navigation

Zrpc

Hex.pm Hex.pm Downloads CI License Documentation

A modern RPC framework for Elixir with a clean DSL, middleware system, and hierarchical routing. Define your API once, generate TypeScript clients and OpenAPI specs automatically.

Zrpc provides a type-safe, transport-agnostic way to define and execute remote procedure calls. It's inspired by tRPC and designed to work seamlessly with Phoenix, Plug, or any Elixir application. Your procedure definitions serve as the single source of truth for validation, documentation, and client generation.

Features

  • Single Source of Truth - Generate TypeScript clients and OpenAPI specs from your procedure definitions
  • Clean DSL for defining queries, mutations, and subscriptions
  • Schema Validation with Zoi for input/output validation
  • Middleware System with compile-time optimization
  • Hierarchical Router with namespacing, scopes, and aliases
  • Transport Agnostic - works with HTTP, WebSocket, or custom transports
  • Telemetry Integration for observability
  • Batch Execution with configurable concurrency

Installation

Add zrpc to your list of dependencies in mix.exs:

def deps do
  [
    {:zrpc, "~> 0.0.0-alpha"}
  ]
end

Quick Start

1. Define Procedures

defmodule MyApp.Procedures.Users do
  use Zrpc.Procedure

  query :get do
    input Zoi.object(%{
      id: Zoi.string() |> Zoi.uuid()
    })

    output Zoi.object(%{
      id: Zoi.string(),
      name: Zoi.string(),
      email: Zoi.string()
    })

    handler fn %{id: id}, _ctx ->
      case MyApp.Users.get(id) do
        nil -> {:error, :not_found}
        user -> {:ok, user}
      end
    end
  end

  mutation :create do
    input Zoi.object(%{
      name: Zoi.string() |> Zoi.min(1),
      email: Zoi.string() |> Zoi.email()
    })

    handler fn input, _ctx ->
      MyApp.Users.create(input)
    end
  end
end

2. Create a Router

defmodule MyApp.Router do
  use Zrpc.Router

  # Global middleware
  middleware MyApp.Middleware.Logger
  middleware MyApp.Middleware.Auth

  # Register procedures at namespaces
  procedures MyApp.Procedures.Users, at: "users"
  procedures MyApp.Procedures.Posts, at: "posts"

  # Scoped routes with additional middleware
  scope "admin" do
    middleware MyApp.Middleware.RequireAdmin

    procedures MyApp.Procedures.Admin, at: "actions"
  end
end

This creates paths like:

  • users.get, users.create
  • posts.list, posts.get
  • admin.actions.delete_user

3. Execute Procedures

# Create a context
ctx = Zrpc.Context.new()

# Single call
{:ok, user} = Zrpc.Router.call(MyApp.Router, "users.get", %{id: "123"}, ctx)

# Batch call
results = Zrpc.Router.batch(MyApp.Router, [
  {"users.get", %{id: "123"}},
  {"posts.list", %{user_id: "123"}}
], ctx)

Core Concepts

Procedures

Procedures are the building blocks of your API. They come in three types:

  • query - Read operations (idempotent)
  • mutation - Write operations
  • subscription - Real-time updates
defmodule MyApp.Procedures.Example do
  use Zrpc.Procedure

  query :fetch_data do
    input Zoi.object(%{id: Zoi.string()})
    handler fn %{id: id}, ctx -> {:ok, %{id: id}} end
  end

  mutation :update_data do
    input Zoi.object(%{id: Zoi.string(), data: Zoi.any()})
    handler fn input, ctx -> {:ok, input} end
  end

  subscription :watch_data do
    input Zoi.object(%{id: Zoi.string()})
    handler fn %{id: id}, ctx ->
      # Return a stream or subscription
    end
  end
end

Context

The context carries request information through the middleware chain and into handlers:

# Create from Plug.Conn
ctx = Zrpc.Context.from_conn(conn)

# Create from Phoenix.Socket
ctx = Zrpc.Context.from_socket(socket)

# Add custom assigns
ctx = Zrpc.Context.assign(ctx, :current_user, user)

# Access in handlers
handler fn input, ctx ->
  user = ctx.assigns[:current_user]
  # ...
end

Middleware

Middleware intercepts procedure calls for cross-cutting concerns:

defmodule MyApp.Middleware.Auth do
  use Zrpc.Middleware

  @impl true
  def call(ctx, _opts, next) do
    case get_current_user(ctx) do
      {:ok, user} ->
        ctx = Zrpc.Context.assign(ctx, :current_user, user)
        next.(ctx)
      {:error, _} ->
        {:error, :unauthorized}
    end
  end
end

Router

The router organizes procedures into a hierarchical namespace:

defmodule MyApp.Router do
  use Zrpc.Router

  # Global middleware
  middleware MyApp.Middleware.RequestId

  # Simple registration
  procedures MyApp.Procedures.Public, at: "public"

  # Nested scopes
  scope "api" do
    scope "v1" do
      procedures MyApp.Procedures.V1.Users, at: "users"
    end
  end

  # Path aliases for backwards compatibility
  path_alias "getUser", to: "api.v1.users.get", deprecated: true
end

Error Handling

Handlers can return errors in multiple formats:

# Simple atom code
{:error, :not_found}

# Code with message
{:error, :validation_failed, "Email is invalid"}

# Structured error
{:error, %{code: :custom_error, message: "Details", extra: "data"}}

Validation errors are automatically formatted:

{:error, %{
  code: :validation_error,
  message: "Validation failed",
  details: %{
    "email" => ["must be a valid email"]
  }
}}

Telemetry Events

Zrpc emits telemetry events for observability:

# Procedure events
[:zrpc, :procedure, :start]
[:zrpc, :procedure, :stop]
[:zrpc, :procedure, :exception]

# Router events
[:zrpc, :router, :lookup, :start]
[:zrpc, :router, :lookup, :stop]
[:zrpc, :router, :batch, :start]
[:zrpc, :router, :batch, :stop]
[:zrpc, :router, :alias, :resolved]

Configuration

# config/config.exs
config :zrpc,
  # Validate procedure output against schema (default: true)
  validate_output: true,

  # Include exception details in error responses (default: false)
  include_exception_details: false

Documentation

License

MIT License - see LICENSE for details. å

About

A modern RPC framework for Elixir with a clean DSL, middleware system, and hierarchical routing. Define your API once, generate TypeScript clients and OpenAPI specs automatically.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages