AshRpc

Expose Ash Resource actions over tRPC with a Plug-compatible router/controller, robust error handling, and schema tooling.

AshRpc is a comprehensive bridge between Ash Framework and tRPC, enabling you to expose your Ash resources as type-safe, performant tRPC endpoints. It provides seamless integration with Phoenix applications, automatic TypeScript generation, and advanced features like field selection and batching.

⚠️ EXPERIMENTAL WARNING: This package is still in early development and considered highly experimental. Breaking changes may occur frequently without notice. We strongly advise against using this package in production environments until it reaches a stable release (v1.0.0+). Use at your own risk for development and testing purposes only.

Table of Contents

Features

🚀 Core Features

🔧 Advanced Capabilities

🛠 Developer Experience

Quick Start

1. Install AshRpc

# If you have Igniter installed (recommended)
mix igniter.install ash_rpc
# Or manually install
mix deps.get
mix ash_rpc.install

This creates your tRPC router and configures your Phoenix router.

2. Configure Your Resources

defmodule MyApp.Accounts.User do
use Ash.Resource, extensions: [AshRpc]
ash_rpc do
expose [:read, :create, :update]
query :read do
filterable true
sortable true
selectable true
paginatable true
end
mutation :create, :create do
metadata fn _subject, user, _ctx ->
%{user_id: user.id}
end
end
end
# ... rest of your resource
end

3. Update Your Router

defmodule MyAppWeb.TrpcRouter do
use AshRpc.Router, domains: [MyApp.Accounts, MyApp.Billing]
end

4. Generate Types

mix ash_rpc.codegen --output=./frontend/generated --zod

5. Use in Frontend

import { createTRPCClient, httpBatchLink } from "@trpc/client";
import type { AppRouter } from "./generated/trpc";
const client = createTRPCClient<AppRouter>({
links: [httpBatchLink({ url: "/trpc" })],
});
// Type-safe API calls
const users = await client.accounts.user.read.query({
filter: { email: { eq: "user@example.com" } },
select: ["id", "email", "name"],
page: { limit: 10, offset: 0 },
});

Installation

Add Dependencies

Add ash_rpc to your mix.exs:

defp deps do
[
{:ash_rpc, "~> 0.1"},
# Recommended for type generation
# For authentication (optional)
{:ash_authentication, "~> 3.0"},
]
end

Install AshRpc

Run the installer to set up your Phoenix application:

mix deps.get
mix ash_rpc.install

This will:

Manual Setup (Alternative)

If you prefer manual setup, create the router manually:

# lib/my_app_web/trpc_router.ex
defmodule MyAppWeb.TrpcRouter do
use AshRpc.Router, domains: [MyApp.Accounts]
end
# router.ex
scope "/trpc" do
pipe_through :ash_rpc
forward "/", MyAppWeb.TrpcRouter
end

Backend Setup

Router Configuration

defmodule MyAppWeb.TrpcRouter do
use AshRpc.Router,
domains: [MyApp.Accounts, MyApp.Billing, MyApp.Notifications],
# Optional: Custom transformer for input/output processing
transformer: MyApp.TrpcTransformer,
# Optional: Before hooks
before: [MyApp.TrpcHooks.Logging],
# Optional: After hooks
after: [MyApp.TrpcHooks.Metrics],
# Optional: Context creation function
create_context: &MyApp.TrpcContext.create/1
end

Resource Configuration

defmodule MyApp.Accounts.User do
use Ash.Resource,
extensions: [AshRpc],
domain: MyApp.Accounts
ash_rpc do
# Expose specific actions
expose [:read, :create, :update, :destroy]
# Or expose all actions
# expose :all
# Custom resource name (defaults to module name)
resource_name "user"
# Configure query procedures
query :read do
filterable true # Allow client-side filtering
sortable true # Allow client-side sorting
selectable true # Allow client-side field selection
paginatable true # Allow client-side pagination
# Custom relationship loading
relationships [:posts, :comments]
end
query :by_email, :read do
# Custom procedure name for specific action
filterable false
selectable true
end
# Configure mutation procedures
mutation :create, :create do
metadata fn _subject, user, _ctx ->
%{user_id: user.id, created_at: user.inserted_at}
end
end
mutation :register, :register_with_password do
metadata fn _subject, user, _ctx ->
%{token: user.__metadata__.token}
end
end
end
# ... resource definition
end

DSL Reference

ash_rpc Block Options

Query Configuration

query :read do
filterable true # Enable filtering (default: true)
sortable true # Enable sorting (default: true)
selectable true # Enable field selection (default: true)
paginatable true # Enable pagination (default: true)
relationships [:posts] # Allow loading specific relationships
end

Mutation Configuration

mutation :create, :create do
metadata fn subject, result, ctx ->
# Return custom metadata in response
%{created_by: subject.id, timestamp: DateTime.utc_now()}
end
end

Authentication

AshRpc integrates seamlessly with AshAuthentication for secure API access.

Setup Authentication

# In your Phoenix router
pipeline :ash_rpc do
plug :accepts, ["json"]
plug :retrieve_from_bearer # Extract token from Authorization header
plug :set_actor, :user # Set current user as actor
end
scope "/trpc" do
pipe_through :ash_rpc
forward "/", MyAppWeb.TrpcRouter
end

Client Authentication

// Include token in requests
const client = createTRPCClient<AppRouter>({
links: [
httpBatchLink({
url: "/trpc",
headers() {
const token = getAuthToken();
return token ? { Authorization: `Bearer ${token}` } : {};
},
}),
],
});

Authorization

AshRpc respects Ash's authorization rules. Configure policies on your resources:

defmodule MyApp.Accounts.User do
# ... resource setup
policies do
policy action_type(:read) do
authorize_if actor_attribute_equals(:role, :admin)
authorize_if relates_to_actor_via(:self)
end
end
end

Frontend Integration

tRPC Client Setup

// client.ts
import { createTRPCClient, httpBatchLink } from "@trpc/client";
import type { AppRouter } from "./generated/trpc";
export function createClient(token?: string) {
return createTRPCClient<AppRouter>({
links: [
httpBatchLink({
url: "/trpc",
headers: token ? { Authorization: `Bearer ${token}` } : {},
}),
],
});
}

React Integration

// App.tsx
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { createTRPCReact } from "@trpc/react-query";
import type { AppRouter } from "./generated/trpc";
export const trpc = createTRPCReact<AppRouter>();
const queryClient = new QueryClient();
function App() {
return (
<trpc.Provider client={createClient()} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
<MyComponent />
</QueryClientProvider>
</trpc.Provider>
);
}

Usage Examples

// UserList.tsx
import { trpc } from "./trpc";
function UserList() {
const { data: users, isLoading } = trpc.accounts.user.read.useQuery({
filter: { role: { eq: "admin" } },
select: ["id", "email", "name"],
sort: { insertedAt: "desc" },
page: { limit: 20, offset: 0 },
});
if (isLoading) return <div>Loading...</div>;
return (
<div>
{users?.result.map((user) => (
<div key={user.id}>{user.name}</div>
))}
</div>
);
}

Mutation Examples

// CreateUser.tsx
import { trpc } from "./trpc";
function CreateUser() {
const createUser = trpc.accounts.user.create.useMutation();
const handleSubmit = async (data: FormData) => {
try {
const result = await createUser.mutateAsync({
email: data.email,
password: data.password,
name: data.name,
});
console.log("Created user:", result.result);
console.log("Metadata:", result.meta);
} catch (error) {
console.error("Failed to create user:", error);
}
};
return <form onSubmit={handleSubmit}>{/* form fields */}</form>;
}

Advanced Features

Batching

AshRpc supports request batching for improved performance:

// Automatic batching with httpBatchLink
const client = createTRPCClient<AppRouter>({
links: [httpBatchLink({ url: "/trpc" })],
});
// Multiple queries batched automatically
const [users, posts] = await Promise.all([
client.accounts.user.read.query({ limit: 10 }),
client.blog.post.read.query({ limit: 10 }),
]);

Field Selection

Dynamically select which fields to return:

// Include specific fields
const users = await client.accounts.user.read.query({
select: ["id", "email", "name"],
});
// Exclude fields with "-"
const users = await client.accounts.user.read.query({
select: ["-password", "-insertedAt"],
});
// Nested field selection
const posts = await client.blog.post.read.query({
select: [
"id",
"title",
{ author: ["name", "email"] },
{ comments: ["content", "-insertedAt"] },
],
});

Filtering & Sorting

Rich query capabilities:

// Complex filtering
const users = await client.accounts.user.read.query({
filter: {
and: [
{ email: { like: "%@company.com" } },
{ or: [{ role: { eq: "admin" } }, { role: { eq: "manager" } }] },
],
},
sort: { insertedAt: "desc" },
});

Pagination

Support for both offset and keyset pagination:

// Offset pagination
const users = await client.accounts.user.read.query({
page: {
type: "offset",
limit: 20,
offset: 40,
count: true, // Include total count
},
});
// Keyset pagination (recommended for large datasets)
const users = await client.accounts.user.read.query({
page: {
type: "keyset",
limit: 20,
after: "cursor_value",
before: "cursor_value",
},
});

TypeScript Generation

Generate Types

# Generate TypeScript types
mix ash_rpc.codegen --output=./frontend/generated
# Generate with Zod schemas
mix ash_rpc.codegen --output=./frontend/generated --zod

Generated Files

Usage

import type { AppRouter } from "./generated/trpc";
import * as schemas from "./generated/trpc.zod";
// Full type safety
const client = createTRPCClient<AppRouter>();
// Client-side validation
const userSchema = schemas.AccountsUserCreateSchema;
const validated = userSchema.parse(formData);

Error Handling

AshRpc provides comprehensive error handling with detailed messages:

try {
await client.accounts.user.create.mutate({
email: "invalid-email", // Missing password
});
} catch (error: any) {
// error.shape?.message - High-level message
// error.data?.details - Array of detailed error objects
console.log(error.shape?.message); // "Validation failed"
error.data?.details.forEach((detail) => {
console.log(detail.message); // "password is required"
console.log(detail.code); // "field_validation_error"
console.log(detail.pointer); // "password"
});
}

API Reference

Router Module

defmodule AshRpc.Router do
@moduledoc """
Main router module for exposing Ash resources via tRPC.
## Options
- `domains`: List of Ash domain modules to expose
- `transformer`: Custom input/output transformer module
- `before`: List of modules to run before request processing
- `after`: List of modules to run after request processing
- `create_context`: Function to create request context
"""
end

DSL Module

defmodule AshRpc do
@moduledoc """
Spark DSL extension for configuring tRPC exposure on Ash resources.
## DSL Structure
ash_rpc do
expose [:action1, :action2]
resource_name "custom_name"
query :action do
filterable true
sortable true
selectable true
paginatable true
relationships [:rel1, :rel2]
end
mutation :action do
metadata fn subject, result, ctx -> %{key: value} end
end
end
"""
end

Examples

Quick Examples

Basic CRUD Operations

# Resource
defmodule MyApp.Blog.Post do
use Ash.Resource, extensions: [AshRpc]
ash_rpc do
expose [:read, :create, :update, :destroy]
query :read do
filterable true
sortable true
selectable true
paginatable true
relationships [:author, :comments]
end
end
end
# Frontend usage
const posts = await client.blog.post.read.query({
filter: { published: { eq: true } },
sort: { publishedAt: "desc" },
select: ["id", "title", "content", { author: ["name"] }],
page: { limit: 10 }
});

Advanced Filtering

// Complex queries with relationships
const posts = await client.blog.post.read.query({
filter: {
and: [
{ published: { eq: true } },
{ author: { name: { like: "John%" } } },
{
or: [
{ tags: { contains: "elixir" } },
{ tags: { contains: "phoenix" } },
],
},
],
},
load: [
{ author: { filter: { active: { eq: true } } } },
{ comments: { sort: { insertedAt: "desc" }, limit: 5 } },
],
});

Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Make your changes
  4. Add tests for new functionality
  5. Run the test suite: mix test
  6. Submit a pull request

Development Setup

git clone https://github.com/antdragon-os/ash_rpc.git
cd ash_rpc
mix deps.get
mix test

Documentation

Documentation is generated with ExDoc. To build locally:

mix docs
open doc/index.html

License

Apache 2.0 - see LICENSE.

Support


Built with ❤️ using Ash Framework and tRPC