Packages
ash_typescript
0.15.2
0.17.3
0.17.2
0.17.1
0.17.0
0.16.0
0.15.3
0.15.2
retired
0.15.1
retired
0.15.0
0.14.4
0.14.3
0.14.2
0.14.1
0.14.0
retired
0.13.2
0.13.1
0.13.0
0.12.1
0.12.0
0.11.6
0.11.5
0.11.4
0.11.3
0.11.2
0.11.1
0.11.0
0.10.2
0.10.1
0.10.0
0.9.1
0.9.0
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.1
0.7.0
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.0
0.4.0
0.3.3
0.3.2
0.3.1
0.2.0
0.1.2
0.1.0
Generate type-safe TypeScript clients directly from your Ash resources and actions, ensuring end-to-end type safety between your backend and frontend.
Retired package: Release invalid - Breaking bug in codegen
Current section
Files
Jump to
Current section
Files
documentation/guides/crud-operations.md
<!--
SPDX-FileCopyrightText: 2025 Torkild G. Kjevik
SPDX-FileCopyrightText: 2025 ash_typescript contributors <https://github.com/ash-project/ash_typescript/graphs/contributors>
SPDX-License-Identifier: MIT
-->
# CRUD Operations
This guide covers Create, Read, Update, and Delete operations using AshTypescript-generated RPC functions.
## Overview
All CRUD operations follow a consistent pattern:
- Field selection using the `fields` parameter
- 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
```typescript
import { listTodos } from './ash_rpc';
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);
}
```
### Get Single Record
```typescript
import { getTodo } from './ash_rpc';
const todo = await getTodo({
fields: ["id", "title", "completed", "priority"],
input: { id: "todo-123" }
});
if (todo.success) {
console.log("Todo:", todo.data);
}
```
### Get by Specific Fields
Use `get_by` actions to lookup records by specific fields:
```elixir
# Elixir configuration
rpc_action :get_user_by_email, :read, get_by: [:email]
```
```typescript
const user = await getUserByEmail({
getBy: { email: "user@example.com" },
fields: ["id", "name", "email"]
});
```
### Handling Not Found
Use `not_found_error?: false` to return `null` instead of an error:
```elixir
# Elixir configuration
rpc_action :find_user, :read, get_by: [:email], not_found_error?: false
```
```typescript
const user = await findUser({
getBy: { email: "maybe@example.com" },
fields: ["id", "name"]
});
if (user.success) {
if (user.data) {
console.log("Found:", user.data.name);
} else {
console.log("User not found");
}
}
```
### With Relationships
Include related data using nested field selection:
```typescript
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);
}
```
### Calculated Fields
Request calculated fields computed by your Ash resource:
```typescript
const todo = await getTodo({
fields: [
"id",
"title",
"dueDate",
"isOverdue", // Boolean calculation
"daysUntilDue" // Integer calculation
],
input: { id: "todo-123" }
});
```
## Create Operations
```typescript
import { createTodo } from './ash_rpc';
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);
} else {
console.error("Failed to create:", newTodo.errors);
}
```
## Update Operations
Update existing records using a **separate identity parameter**:
```typescript
import { updateTodo } from './ash_rpc';
const updatedTodo = await updateTodo({
fields: ["id", "title", "priority", "updatedAt"],
identity: "todo-123", // Identity as separate parameter
input: {
title: "Updated: Learn AshTypescript",
priority: "urgent"
}
});
```
**Important**: The `identity` parameter is separate from the `input` object. This ensures identity fields cannot be accidentally modified.
### Update with Named Identities
Configure update actions to use named identities instead of the primary key:
```elixir
# Elixir configuration
rpc_action :update_user_by_email, :update, identities: [:email]
```
```typescript
const updated = await updateUserByEmail({
identity: { email: "user@example.com" },
input: { name: "New Name" },
fields: ["id", "name"]
});
```
See [RPC Action Options](../features/rpc-action-options.md) for detailed identity configuration.
## Delete Operations
```typescript
import { destroyTodo } from './ash_rpc';
const deletedTodo = await destroyTodo({
identity: "todo-123"
});
if (deletedTodo.success) {
console.log("Todo deleted successfully");
}
```
### Delete with Named Identities
```elixir
# Elixir configuration
rpc_action :destroy_user_by_email, :destroy, identities: [:email]
```
```typescript
await destroyUserByEmail({
identity: { email: "user@example.com" }
});
```
## Error Handling
All RPC functions return a `{success: true/false}` structure:
```typescript
const result = await createTodo({
fields: ["id", "title"],
input: { title: "New Todo", userId: "user-id-123" }
});
if (result.success) {
console.log("Created:", result.data);
} else {
result.errors.forEach(error => {
console.error(`Error: ${error.message}`);
if (error.fields.length > 0) {
console.error(`Fields: ${error.fields.join(', ')}`);
}
});
}
```
See [Error Handling](error-handling.md) for comprehensive error handling strategies.
## Authentication and Headers
All RPC functions accept optional headers:
```typescript
import { listTodos, buildCSRFHeaders } from './ash_rpc';
// With CSRF protection
const todos = await listTodos({
fields: ["id", "title"],
headers: buildCSRFHeaders()
});
// With Bearer token
const todos = await listTodos({
fields: ["id", "title"],
headers: {
"Authorization": "Bearer your-token-here"
}
});
// Combined
const todos = await listTodos({
fields: ["id", "title"],
headers: {
...buildCSRFHeaders(),
"Authorization": "Bearer your-token-here"
}
});
```
## Complete Example
```typescript
import {
listTodos,
getTodo,
createTodo,
updateTodo,
destroyTodo,
buildCSRFHeaders
} from './ash_rpc';
const headers = buildCSRFHeaders();
// 1. Create
const createResult = await createTodo({
fields: ["id", "title", "createdAt"],
input: { title: "Learn AshTypescript CRUD", priority: "high", userId: "user-123" },
headers
});
if (!createResult.success) return;
const todoId = createResult.data.id;
// 2. Read (single)
const getResult = await getTodo({
fields: ["id", "title", "priority", { user: ["name"] }],
input: { id: todoId },
headers
});
// 3. Read (list)
const listResult = await listTodos({
fields: ["id", "title", "completed"],
filter: { completed: { eq: false } },
headers
});
// 4. Update
const updateResult = await updateTodo({
fields: ["id", "title", "updatedAt"],
identity: todoId,
input: { title: "Mastered AshTypescript CRUD" },
headers
});
// 5. Delete
const deleteResult = await destroyTodo({
identity: todoId,
headers
});
```
## Next Steps
- [Field Selection](field-selection.md) - Advanced field selection patterns
- [Querying Data](querying-data.md) - Pagination, sorting, and filtering
- [Error Handling](error-handling.md) - Comprehensive error handling
- [RPC Action Options](../features/rpc-action-options.md) - Identity lookups, load restrictions
- [Custom Fetch](../advanced/custom-fetch.md) - Request customization and interceptors