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,
  };
};
💡
Type Safety

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 failed
  • errors – 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' });
⚠️
ID is Required

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 items
  • client.models.Model.get() – Get single item by ID
  • client.models.Model.list() – List items with optional filters
  • client.models.Model.update() – Update existing items
  • client.models.Model.delete() – Delete items
  • Always check for errors in the response
  • Use subscriptions for real-time updates