CRUD Operations
Learn how to Create, Read, Update, and Delete data using the Amplify client library. We'll build a complete data composable for our Todo app.
Setting Up the Client
First, create a data composable at composables/useAmplifyData.ts:
// composables/useAmplifyData.ts
import { generateClient } from 'aws-amplify/data';
import type { Schema } from '@/amplify/data/resource';
export const useAmplifyData = () => {
const client = generateClient<Schema>();
return {
client,
};
};
By passing the Schema type to generateClient,
you get full TypeScript autocomplete and type checking for all
data operations.
Create (Insert)
To create a new Todo item:
const { client } = useAmplifyData();
// Create a new todo
const createTodo = async () => {
const { data, errors } = await client.models.Todo.create({
content: 'Buy groceries',
completed: false,
priority: 'MEDIUM',
});
if (errors) {
console.error('Failed to create todo:', errors);
return null;
}
console.log('Created todo:', data);
return data;
};
The Response Object
All client operations return an object with:
data– The returned item(s), or null if failederrors– Array of errors, or undefined if successful
// Always check for errors
const { data, errors } = await client.models.Todo.create({...});
if (errors) {
// Handle errors
errors.forEach(error => console.error(error.message));
return;
}
// Use data safely
console.log(data.id);
Read (Query)
Get Single Item by ID
const getTodo = async (id: string) => {
const { data, errors } = await client.models.Todo.get({ id });
if (errors) {
console.error('Failed to get todo:', errors);
return null;
}
return data;
};
List All Items
const listTodos = async () => {
const { data, errors } = await client.models.Todo.list();
if (errors) {
console.error('Failed to list todos:', errors);
return [];
}
return data;
};
List with Filters
// Get only incomplete todos
const listIncompleteTodos = async () => {
const { data, errors } = await client.models.Todo.list({
filter: {
completed: { eq: false },
},
});
return data || [];
};
// Get high priority todos
const listHighPriorityTodos = async () => {
const { data, errors } = await client.models.Todo.list({
filter: {
priority: { eq: 'HIGH' },
},
});
return data || [];
};
// Combine filters (AND)
const listUrgentTodos = async () => {
const { data, errors } = await client.models.Todo.list({
filter: {
completed: { eq: false },
priority: { eq: 'HIGH' },
},
});
return data || [];
};
Filter Operators
| Operator | Description | Example |
|---|---|---|
eq |
Equals | { completed: { eq: true } } |
ne |
Not equals | { status: { ne: 'DELETED' } } |
gt |
Greater than | { count: { gt: 10 } } |
ge |
Greater or equal | { count: { ge: 10 } } |
lt |
Less than | { count: { lt: 10 } } |
le |
Less or equal | { count: { le: 10 } } |
contains |
String contains | { content: { contains: 'buy' } } |
beginsWith |
String starts with | { content: { beginsWith: 'Buy' } } |
Update
const updateTodo = async (id: string, updates: Partial<Todo>) => {
const { data, errors } = await client.models.Todo.update({
id,
...updates,
});
if (errors) {
console.error('Failed to update todo:', errors);
return null;
}
return data;
};
// Example usage
await updateTodo('abc123', { completed: true });
await updateTodo('abc123', { content: 'Updated content', priority: 'HIGH' });
You must always include the id field when updating.
Only the fields you include will be updated; others remain unchanged.
Delete
const deleteTodo = async (id: string) => {
const { data, errors } = await client.models.Todo.delete({ id });
if (errors) {
console.error('Failed to delete todo:', errors);
return false;
}
console.log('Deleted todo:', data);
return true;
};
Complete Todo Composable
Here's a complete composable for managing todos:
// composables/useTodos.ts
import { generateClient } from 'aws-amplify/data';
import type { Schema } from '@/amplify/data/resource';
type Todo = Schema['Todo']['type'];
type CreateTodoInput = Schema['Todo']['createType'];
type UpdateTodoInput = Schema['Todo']['updateType'];
export const useTodos = () => {
const client = generateClient<Schema>();
// Reactive state
const todos = useState<Todo[]>('todos', () => []);
const isLoading = useState('todos-loading', () => false);
const error = useState<string | null>('todos-error', () => null);
// Fetch all todos
const fetchTodos = async () => {
isLoading.value = true;
error.value = null;
try {
const { data, errors } = await client.models.Todo.list();
if (errors) {
error.value = errors[0]?.message || 'Failed to fetch todos';
return;
}
todos.value = data || [];
} catch (e: any) {
error.value = e.message || 'An error occurred';
} finally {
isLoading.value = false;
}
};
// Create a todo
const createTodo = async (input: Omit<CreateTodoInput, 'id'>) => {
error.value = null;
try {
const { data, errors } = await client.models.Todo.create(input);
if (errors) {
error.value = errors[0]?.message || 'Failed to create todo';
return null;
}
// Add to local state
if (data) {
todos.value = [...todos.value, data];
}
return data;
} catch (e: any) {
error.value = e.message || 'An error occurred';
return null;
}
};
// Update a todo
const updateTodo = async (id: string, updates: Partial<UpdateTodoInput>) => {
error.value = null;
try {
const { data, errors } = await client.models.Todo.update({
id,
...updates,
});
if (errors) {
error.value = errors[0]?.message || 'Failed to update todo';
return null;
}
// Update local state
if (data) {
todos.value = todos.value.map(t =>
t.id === id ? data : t
);
}
return data;
} catch (e: any) {
error.value = e.message || 'An error occurred';
return null;
}
};
// Toggle todo completion
const toggleTodo = async (id: string) => {
const todo = todos.value.find(t => t.id === id);
if (!todo) return null;
return updateTodo(id, { completed: !todo.completed });
};
// Delete a todo
const deleteTodo = async (id: string) => {
error.value = null;
try {
const { errors } = await client.models.Todo.delete({ id });
if (errors) {
error.value = errors[0]?.message || 'Failed to delete todo';
return false;
}
// Remove from local state
todos.value = todos.value.filter(t => t.id !== id);
return true;
} catch (e: any) {
error.value = e.message || 'An error occurred';
return false;
}
};
return {
// State
todos,
isLoading,
error,
// Actions
fetchTodos,
createTodo,
updateTodo,
toggleTodo,
deleteTodo,
};
};
Using in a Component
<script setup lang="ts">
const {
todos,
isLoading,
error,
fetchTodos,
createTodo,
toggleTodo,
deleteTodo
} = useTodos();
const newTodoContent = ref('');
// Fetch todos on mount
onMounted(() => {
fetchTodos();
});
const handleSubmit = async () => {
if (!newTodoContent.value.trim()) return;
await createTodo({
content: newTodoContent.value,
completed: false,
});
newTodoContent.value = '';
};
</script>
<template>
<div class="todo-app">
<h1>My Todos</h1>
<form @submit.prevent="handleSubmit" class="add-todo">
<input
v-model="newTodoContent"
type="text"
placeholder="What needs to be done?"
:disabled="isLoading"
/>
<button type="submit" :disabled="isLoading">Add</button>
</form>
<p v-if="error" class="error">{{ error }}</p>
<p v-if="isLoading">Loading...</p>
<ul v-else class="todo-list">
<li v-for="todo in todos" :key="todo.id" class="todo-item">
<input
type="checkbox"
:checked="todo.completed"
@change="toggleTodo(todo.id)"
/>
<span :class="{ completed: todo.completed }">
{{ todo.content }}
</span>
<button @click="deleteTodo(todo.id)" class="delete-btn">
Delete
</button>
</li>
</ul>
<p v-if="!isLoading && todos.length === 0">
No todos yet. Add one above!
</p>
</div>
</template>
Real-Time Subscriptions
Amplify supports real-time updates via GraphQL subscriptions. Here's how to listen for changes:
// Subscribe to new todos
const subscribeToTodos = () => {
const subscription = client.models.Todo.onCreate().subscribe({
next: (data) => {
console.log('New todo created:', data);
// Add to local state if not already present
if (!todos.value.find(t => t.id === data.id)) {
todos.value = [...todos.value, data];
}
},
error: (error) => {
console.error('Subscription error:', error);
},
});
// Return unsubscribe function
return () => subscription.unsubscribe();
};
// In component
onMounted(() => {
fetchTodos();
const unsubscribe = subscribeToTodos();
// Clean up on unmount
onUnmounted(() => {
unsubscribe();
});
});
Summary
- Use
generateClient<Schema>()for type-safe operations client.models.Model.create()– Create new itemsclient.models.Model.get()– Get single item by IDclient.models.Model.list()– List items with optional filtersclient.models.Model.update()– Update existing itemsclient.models.Model.delete()– Delete items- Always check for errors in the response
- Use subscriptions for real-time updates