Basic CRUD Operations
View SourceThis guide covers the fundamental Create, Read, Update, and Delete operations using AshTypescript-generated RPC functions.
Overview
All CRUD operations follow a consistent pattern:
- Field selection using the 
fieldsparameter - Type-safe input/output based on your Ash resources
 - Explicit error handling with 
{success: true/false}return values - Support for relationships and nested field selection
 
List/Read Operations
List Multiple Records
Use list operations to retrieve multiple records with filtering and sorting:
import { listTodos } from './ash_rpc';
// List todos with field selection
const todos = await listTodos({
  fields: ["id", "title", "completed", "priority"],
  filter: { completed: { eq: false } },
  sort: "-priority,+createdAt"
});
if (todos.success) {
  console.log("Found todos:", todos.data);
  todos.data.forEach(todo => {
    console.log(`${todo.id}: ${todo.title}`);
  });
}Get Single Record
Retrieve a single record by its identifier:
import { getTodo } from './ash_rpc';
// Get single todo with basic fields
const todo = await getTodo({
  fields: ["id", "title", "completed", "priority"],
  input: { id: "todo-123" }
});
if (todo.success) {
  console.log("Todo:", todo.data);
}Get with Relationships
Include related data using nested field selection:
// Get single todo with relationships
const todo = await getTodo({
  fields: [
    "id",
    "title",
    { user: ["name", "email"] }
  ],
  input: { id: "todo-123" }
});
if (todo.success) {
  console.log("Todo:", todo.data.title);
  console.log("Created by:", todo.data.user.name);
}Advanced Field Selection
Use complex nested structures for detailed data retrieval:
// Complex nested field selection
const todoWithDetails = await getTodo({
  fields: [
    "id", "title", "description", "tags",
    {
      user: ["id", "name", "email"],
      comments: ["id", "content", "authorName"]
    }
  ],
  input: { id: "todo-123" }
});
if (todoWithDetails.success) {
  console.log("Todo:", todoWithDetails.data.title);
  console.log("Comments:", todoWithDetails.data.comments.length);
  console.log("Tags:", todoWithDetails.data.tags); // Array of strings
  todoWithDetails.data.comments.forEach(comment => {
    console.log(`Comment by ${comment.authorName}: ${comment.content}`);
  });
}Calculated Fields
Request calculated fields that are computed by your Ash resource:
// Calculated fields
const todoWithCalc = await getTodo({
  fields: [
    "id",
    "title",
    "dueDate",
    "isOverdue",      // Boolean calculation
    "daysUntilDue"    // Integer calculation
  ],
  input: { id: "todo-123" }
});
if (todoWithCalc.success) {
  console.log("Todo:", todoWithCalc.data.title);
  console.log("Due date:", todoWithCalc.data.dueDate);
  console.log("Is overdue:", todoWithCalc.data.isOverdue);
  console.log("Days until due:", todoWithCalc.data.daysUntilDue);
}Create Operations
Create new records with type-safe input validation:
import { createTodo } from './ash_rpc';
// Create new todo
const newTodo = await createTodo({
  fields: ["id", "title", "createdAt"],
  input: {
    title: "Learn AshTypescript",
    priority: "high",
    dueDate: "2024-01-01",
    userId: "user-id-123"
  }
});
if (newTodo.success) {
  console.log("Created todo:", newTodo.data);
  console.log("ID:", newTodo.data.id);
  console.log("Created at:", newTodo.data.createdAt);
} else {
  console.error("Failed to create todo:", newTodo.errors);
}Update Operations
Update existing records using a separate primary key parameter:
import { updateTodo } from './ash_rpc';
// Update existing todo (primary key separate from input)
const updatedTodo = await updateTodo({
  fields: ["id", "title", "priority", "updatedAt"],
  primaryKey: "todo-123",  // Primary key as separate parameter
  input: {
    title: "Updated: Learn AshTypescript",
    priority: "urgent"
  }
});
if (updatedTodo.success) {
  console.log("Updated todo:", updatedTodo.data);
  console.log("New title:", updatedTodo.data.title);
  console.log("Updated at:", updatedTodo.data.updatedAt);
} else {
  console.error("Failed to update:", updatedTodo.errors);
}Important: The primaryKey parameter is separate from the input object. This ensures that the primary key cannot be accidentally modified.
Delete Operations
Delete records using the primary key parameter:
import { destroyTodo } from './ash_rpc';
// Delete todo (primary key separate from input)
const deletedTodo = await destroyTodo({
  primaryKey: "todo-123"    // Primary key as separate parameter
});
if (deletedTodo.success) {
  console.log("Todo deleted successfully");
} else {
  console.error("Failed to delete:", deletedTodo.errors);
}Error Handling
All generated RPC functions return a {success: true/false} structure instead of throwing exceptions:
const result = await createTodo({
  fields: ["id", "title"],
  input: {
    title: "New Todo",
    userId: "user-id-123"
  }
});
if (result.success) {
  // Access the created todo
  console.log("Created todo:", result.data);
  const todoId: string = result.data.id;
  const todoTitle: string = result.data.title;
} else {
  // Handle validation errors, network errors, etc.
  result.errors.forEach(error => {
    console.error(`Error: ${error.message}`);
    if (error.fieldPath) {
      console.error(`Field: ${error.fieldPath}`);
    }
  });
}Common Error Scenarios
// Validation errors (e.g., missing required fields)
const result = await createTodo({
  fields: ["id", "title"],
  input: {}  // Missing required title and userId
});
if (!result.success) {
  result.errors.forEach(error => {
    console.error(`${error.fieldPath}: ${error.message}`);
    // Output: "title: is required"
  });
}
// Not found errors
const result = await getTodo({
  fields: ["id", "title"],
  input: { id: "nonexistent-id" }
});
if (!result.success) {
  console.error("Todo not found");
}Custom Headers and Authentication
All RPC functions accept optional headers for authentication and other purposes:
import { listTodos, buildCSRFHeaders } from './ash_rpc';
// With CSRF protection
const todos = await listTodos({
  fields: ["id", "title"],
  headers: buildCSRFHeaders()
});
// With custom authentication
const todos = await listTodos({
  fields: ["id", "title"],
  headers: {
    "Authorization": "Bearer your-token-here",
    "X-Custom-Header": "value"
  }
});
// Combining headers
const todos = await listTodos({
  fields: ["id", "title"],
  headers: {
    ...buildCSRFHeaders(),
    "Authorization": "Bearer your-token-here"
  }
});Custom Fetch Functions and Request Options
Using fetchOptions for Request Customization
All generated RPC functions accept an optional fetchOptions parameter that allows you to customize the underlying fetch request:
import { createTodo, listTodos } from './ash_rpc';
// Add request timeout and custom cache settings
const todo = await createTodo({
  fields: ["id", "title"],
  input: {
    title: "New Todo",
    userId: "user-id-123"
  },
  fetchOptions: {
    signal: AbortSignal.timeout(5000), // 5 second timeout
    cache: 'no-cache',
    credentials: 'include'
  }
});
// Use with abort controller for cancellable requests
const controller = new AbortController();
const todos = await listTodos({
  fields: ["id", "title"],
  fetchOptions: {
    signal: controller.signal
  }
});
// Cancel the request if needed
controller.abort();Custom Fetch Functions
You can replace the native fetch function entirely by providing a customFetch parameter. This is useful for:
- Adding global authentication
 - Using alternative HTTP clients like axios
 - Adding request/response interceptors
 - Custom error handling
 
// Custom fetch with user preferences and tracking
const enhancedFetch = async (url: RequestInfo | URL, init?: RequestInit) => {
  // Get user preferences from localStorage (safe, non-sensitive data)
  const userLanguage = localStorage.getItem('userLanguage') || 'en';
  const userTimezone = localStorage.getItem('userTimezone') || 'UTC';
  const apiVersion = localStorage.getItem('preferredApiVersion') || 'v1';
  // Generate correlation ID for request tracking
  const correlationId = `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
  const customHeaders = {
    'Accept-Language': userLanguage,
    'X-User-Timezone': userTimezone,
    'X-API-Version': apiVersion,
    'X-Correlation-ID': correlationId,
  };
  return fetch(url, {
    ...init,
    headers: {
      ...init?.headers,
      ...customHeaders
    }
  });
};
// Use custom fetch function
const todos = await listTodos({
  fields: ["id", "title"],
  customFetch: enhancedFetch
});Using Axios with AshTypescript
While AshTypescript uses the fetch API by default, you can create an adapter to use axios or other HTTP clients:
import axios from 'axios';
// Create axios adapter that matches fetch API
const axiosAdapter = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
  try {
    const url = typeof input === 'string' ? input : input.toString();
    const axiosResponse = await axios({
      url,
      method: init?.method || 'GET',
      headers: init?.headers,
      data: init?.body,
      timeout: 10000,
      // Add other axios-specific options
      validateStatus: () => true // Don't throw on HTTP errors
    });
    // Convert axios response to fetch Response
    return new Response(JSON.stringify(axiosResponse.data), {
      status: axiosResponse.status,
      statusText: axiosResponse.statusText,
      headers: new Headers(axiosResponse.headers as any)
    });
  } catch (error) {
    if (error.response) {
      // HTTP error status
      return new Response(JSON.stringify(error.response.data), {
        status: error.response.status,
        statusText: error.response.statusText
      });
    }
    throw error; // Network error
  }
};
// Use axios for all requests
const todos = await listTodos({
  fields: ["id", "title"],
  customFetch: axiosAdapter
});Complete CRUD Example
Here's a complete example demonstrating all CRUD operations:
import {
  listTodos,
  getTodo,
  createTodo,
  updateTodo,
  destroyTodo,
  buildCSRFHeaders
} from './ash_rpc';
const headers = buildCSRFHeaders();
// 1. Create a new todo
const createResult = await createTodo({
  fields: ["id", "title", "createdAt"],
  input: {
    title: "Learn AshTypescript CRUD",
    priority: "high",
    userId: "user-id-123"
  },
  headers
});
if (!createResult.success) {
  console.error("Failed to create:", createResult.errors);
  return;
}
const todoId = createResult.data.id;
console.log("Created:", createResult.data);
// 2. Read the todo
const getResult = await getTodo({
  fields: ["id", "title", "priority", { user: ["name"] }],
  input: { id: todoId },
  headers
});
if (getResult.success) {
  console.log("Retrieved:", getResult.data);
}
// 3. Update the todo
const updateResult = await updateTodo({
  fields: ["id", "title", "priority", "updatedAt"],
  primaryKey: todoId,
  input: {
    title: "Mastered AshTypescript CRUD",
    priority: "completed"
  },
  headers
});
if (updateResult.success) {
  console.log("Updated:", updateResult.data);
}
// 4. List all completed todos
const listResult = await listTodos({
  fields: ["id", "title", "priority"],
  filter: { completed: { eq: true } },
  headers
});
if (listResult.success) {
  console.log("Completed todos:", listResult.data.length);
}
// 5. Delete the todo
const deleteResult = await destroyTodo({
  primaryKey: todoId,
  headers
});
if (deleteResult.success) {
  console.log("Deleted successfully");
}Next Steps
- Learn about Phoenix Channel-based RPC actions for real-time communication
 - Explore field selection patterns for complex queries
 - Review error handling strategies for production applications
 - Learn about custom fetch functions for adding authentication and request customization