~/.claude/skills/ or any Agent-Skills-compatible runtime for AI-assisted CRUD work.
CRUD
Glossary
- CRUD — the four verbs (Create, Read, Update, Delete) every persistent collection supports.
- Action triple —
_REQUEST,_SUCCESS,_FAILvariants of one verb that model the start, happy path, and error path of an async call.- Domain prefix — a feature scope like
[Todo]ortodo/that namespaces actions so two slices can share a verb without colliding.
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 triple — CREATE_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
References
- CRUD on Wikipedia — origin of the acronym and its mapping to persistence verbs.
- Redux Style Guide — official guidance on action naming, including the request/success/failure pattern.
- NgRx Action Hygiene — the
[Source] Eventnaming convention used bychota-angular-ngrx. - Redux Toolkit
createAsyncThunk— auto-generatedpending/fulfilled/rejectedtriple poweringchota-react-rtk. - Redux-Saga
takeLatest— howchota-react-sagawatches a CRUD verb and runs the operation that emits the success/error action. - Pinia stores — option-store form (
state+actions) used bychota-vue-piniato colocate the triple as plain methods. - REST vs CRUD — how the four verbs translate to HTTP, useful when designing the API your actions call.