Skip to the content.
Agent Skill Available Download this Agent Skill (SKILL.md) to drop into ~/.claude/skills/ or any Agent-Skills-compatible runtime for AI-assisted CRUD work.
Download
crud

CRUD

Glossary

Detailed Description

CRUD is the lingua franca of persistence. Pick a noun in your app — todos, users, invoices — and the user-facing operations almost always reduce to creating new ones, reading the list, updating individual rows, and deleting them. Naming your state actions after these four verbs keeps the action log readable: anyone scanning a Redux DevTools timeline can guess what CREATE_TODO and DELETE_TODO do without opening a reducer.

The mapping holds across storage layers, which is why every template in this repo reuses the same vocabulary on the client:

CRUD SQL HTTP Redux action NgRx action
Create INSERT POST CREATE_TODO [Todo] CreateTodo
Read SELECT GET READ_TODO [Todo] LoadTodos
Update UPDATE PUT UPDATE_TODO [Todo] UpdateTodo
Delete DELETE DELETE DELETE_TODO [Todo] DeleteTodo

Async CRUD needs more than four actions, though. Once a request leaves the browser, the UI has three things to render: the in-flight spinner, the resolved data, and the error. The Redux family templates encode that as the request/success/fail tripleCREATE_TODO, CREATE_TODO_SUCCESS, CREATE_TODO_ERROR — so the reducer can flip a isLoading flag on request, commit data on success, and surface an error string on failure. NgRx spells it addTodoRequest / addTodoSuccess / addTodoFail; RTK’s createAsyncThunk generates pending/fulfilled/rejected automatically.

The exception is purely-local actions like EDIT_TODO (stage the row being edited) and TOGGLE_TODO (flip the checkbox). Those mutate UI state synchronously and don’t need a triple. The rule of thumb: if it touches the network or another tab, give it a triple; if it only touches the current store, a single action is fine.

Key Insight

A CRUD action name has two axes: the verb (what the user wants) and the lifecycle phase (where in the round-trip we are). One axis without the other leaves your reducer guessing. CREATE_TODO alone can’t tell loading from done; SUCCESS alone can’t tell create from update. The triple is the smallest naming scheme that pins down both.

Basic Example

React Redux

// templates/chota-react-redux/src/state/todo/todo.type.js
export const CREATE_TODO = "CREATE_TODO";
export const READ_TODO   = "READ_TODO";
export const UPDATE_TODO = "UPDATE_TODO";
export const TOGGLE_TODO = "TOGGLE_TODO";
export const DELETE_TODO = "DELETE_TODO";

// classic Redux keeps action types as constants so typos
// surface at import time rather than at runtime

React RTK

// templates/chota-react-rtk/src/state/todo/todo.actions.js
import { todoSlice } from "./todo.reducer";

export const {
  createTodo,
  editTodo,
  updateTodo,
  deleteTodo,
  toggleTodo,
} = todoSlice.actions;
// createAsyncThunk inside the slice generates
// pending / fulfilled / rejected automatically

React Saga

// templates/chota-react-saga/src/state/todo/todo.actions.js
export const createTodo = (text) => ({
  type: CREATE_TODO,
  payload: { text, completed: false },
});
export const createTodoSuccess = (payload) => ({
  type: CREATE_TODO_SUCCESS,
  payload,
});
export const createTodoError = (error) => ({
  type: CREATE_TODO_ERROR,
  error,
});

NgRx (Angular)

// templates/chota-angular-ngrx/src/app/state/todo/todo.actions.ts
export const addTodoRequest = createAction(
  '[Todo] AddTodoRequest',
  props<{ text: string }>()
);
export const addTodoSuccess = createAction('[Todo] AddTodoSuccess');
export const addTodoFail = createAction(
  '[Todo] AddTodoFail',
  props<{ error: string }>()
);

Pinia (Vue)

// templates/chota-vue-pinia/src/state/todo/todo.actions.js
export function createTodo(text) {
  this.isLoading = true;
  this.isActionLoading = true;
  this.currentTodoItem = { text, completed: false };
}
export function createTodoSuccess(payload) {
  this.isLoading = false;
  this.todoItems.push({ id: payload.id, text: payload.text, completed: false });
}
export function createTodoError(error) {
  this.isLoading = false;
  this.error = error;
}

Practical Example

A focused optimistic update flow using the saga template. Toggling a todo flips the checkbox immediately, then reconciles with the server — and rolls back if the request fails. The triple is what makes rollback possible: the success action clears the snapshot, the error action restores it.

// templates/chota-react-saga/src/state/todo/todo.operations.js
export function* updateToggleTodos(action) {
  try {
    yield call(updateTodoApi, toggleCheckedState(action.payload));
    yield put(toggleTodoSuccess());
  } catch (error) {
    // brief delay so the optimistic flip is visible before rollback
    yield delay(500);
    const previous = yield select(
      (state) => state.todo.previousStateTodoItems
    );
    yield put(toggleTodoError(previous, error.toString()));
  }
}

// reducer stashes the pre-toggle list when TOGGLE_TODO fires,
// drops it on TOGGLE_TODO_SUCCESS, and restores from it on
// TOGGLE_TODO_ERROR. Three actions, three reducer branches —
// one verb, one lifecycle.

The UI dispatches a single toggleTodo(item). Saga intercepts, calls the API, and emits success or error. Without the triple you’d either block the UI on the network (no optimism) or have no way to undo the flip when the server rejects it.

Common Mistakes

1. Collapsing the lifecycle into one action

// BAD — one action covers request, success, and error
dispatch({ type: 'CREATE_TODO', payload: newTodo });
// reducer has no idea whether to show a spinner, commit the row,
// or render an error banner — it's all the same action.
// GOOD — three actions, one per lifecycle phase
dispatch(createTodo(text));               // CREATE_TODO         → isLoading = true
// saga / thunk runs, then…
dispatch(createTodoSuccess(saved));       // CREATE_TODO_SUCCESS → push row, clear flag
// or on failure…
dispatch(createTodoError(err.toString()));// CREATE_TODO_ERROR   → set error, clear flag

Why it matters: the loading flag, the data commit, and the error message live in different reducer branches. One action means the reducer can’t distinguish them, which means the UI can’t render a spinner, an empty state, or an error toast without ad-hoc booleans bolted on top.

2. Naming actions after database rows instead of intents

// BAD — action sounds like a record, not an event
dispatch({ type: 'TODO_ROW', payload: { id: 7, text: 'milk' } });
// What happened to the row? Was it inserted? Edited? Loaded from cache?
// GOOD — action names a user intent, payload carries the record
dispatch(createTodo('milk'));    // intent: add a new todo
dispatch(updateTodo({ id: 7, text: 'oat milk' })); // intent: edit row 7
dispatch(deleteTodo(7));         // intent: remove row 7

Why it matters: actions are events in a log, not entities in a table. The DevTools timeline should read like a story (CREATE_TODO, READ_TODO_SUCCESS, TOGGLE_TODO) — not a dump of rows. Naming by intent also makes sagas and effects easier: you takeLatest(CREATE_TODO, addTodos) because that’s the verb you’re reacting to.

3. Forgetting to invalidate cached lists after Create or Delete

// BAD — DELETE_TODO_SUCCESS only flips the loading flag
case DELETE_TODO_SUCCESS:
  return { ...state, isLoading: false };
// the deleted row is still in state.todoItems until the next READ_TODO
// GOOD — Create / Delete success actions also reconcile the list
case DELETE_TODO_SUCCESS:
  return {
    ...state,
    isLoading: false,
    todoItems: state.todoItems.filter((t) => t.id !== action.payload.id),
    previousStateTodoItems: undefined, // drop the rollback snapshot
  };

Why it matters: Read-shaped caches (state.todoItems, RTK Query’s getTodos cache, an NgRx entity adapter’s collection) don’t auto-update when a sibling mutation succeeds. Either invalidate the cache, optimistically patch it, or refetch — but pick one. Skipping this step is the single most common reason a “deleted” todo reappears after a route change.

Quick Quiz

Which CRUD verb maps to HTTP POST and SQL INSERT?

Why split an async create into CREATE_TODO, CREATE_TODO_SUCCESS, and CREATE_TODO_ERROR?

Which action in the templates does NOT use a request/success/fail triple, and why?

In NgRx, what does the bracketed prefix '[Todo]' in '[Todo] CreateTodo' represent?

After DELETE_TODO_SUCCESS fires, the deleted row still shows up until the next page load. What's the most likely cause?

References