Step 6: Data Fetching
In this step, you will add an HTTP client, fetch todos from a mock API with createResource, handle loading and error states, and use createAction for mutations with optimistic updates.
Set Up a Mock API
For this tutorial, we will create a simple in-memory mock server. In a real app, you would point this at your actual backend.
Create src/api/mock-server.ts:
import type { Todo } from '../types';
// In-memory store simulating a database
let db: Todo[] = [
{ id: '1', text: 'Learn AkashJS', completed: true, createdAt: Date.now() - 100000 },
{ id: '2', text: 'Build a todo app', completed: false, createdAt: Date.now() - 50000 },
{ id: '3', text: 'Deploy to production', completed: false, createdAt: Date.now() },
];
// Simulate network delay
function delay(ms = 300): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
export const mockApi = {
async getTodos(): Promise<Todo[]> {
await delay();
return [...db];
},
async addTodo(text: string): Promise<Todo> {
await delay();
const todo: Todo = {
id: crypto.randomUUID(),
text,
completed: false,
createdAt: Date.now(),
};
db.push(todo);
return todo;
},
async toggleTodo(id: string): Promise<Todo> {
await delay();
const todo = db.find(t => t.id === id);
if (!todo) throw new Error(`Todo ${id} not found`);
todo.completed = !todo.completed;
return { ...todo };
},
async deleteTodo(id: string): Promise<void> {
await delay();
db = db.filter(t => t.id !== id);
},
async updateTodo(id: string, text: string): Promise<Todo> {
await delay();
const todo = db.find(t => t.id === id);
if (!todo) throw new Error(`Todo ${id} not found`);
todo.text = text;
return { ...todo };
},
};Create the HTTP Client
Create src/api/client.ts:
import { createHttpClient } from '@akashjs/http';
// In a real app, this would point to your API server:
// export const http = createHttpClient({ baseUrl: '/api' });
//
// For this tutorial, we use the mock API directly.
// The HTTP client is shown here so you see the real pattern.
export const http = createHttpClient({
baseUrl: 'https://api.example.com',
headers: {
'Content-Type': 'application/json',
},
});In production
With a real backend, your fetch calls would look like:
const todos = await http.get<Todo[]>('/todos');
const newTodo = await http.post<Todo>('/todos', { text: 'Hello' });
await http.delete('/todos/123');The createHttpClient provides typed .get(), .post(), .put(), .patch(), and .delete() methods, plus interceptor support for auth tokens, logging, and error handling.
Create a Resource for Fetching Todos
Create src/api/todos.ts:
import { createResource } from '@akashjs/http';
import { createAction } from '@akashjs/http';
import { mockApi } from './mock-server';
import type { Todo } from '../types';
// Reactive resource — fetches todos and exposes loading/error states
export const todosResource = createResource<Todo[]>(
() => mockApi.getTodos(),
{
initialData: [],
staleTime: 30_000, // Consider data fresh for 30 seconds
},
);
// Action: add a new todo
export const addTodoAction = createAction(
(text: string) => mockApi.addTodo(text),
{
onSuccess: () => {
todosResource.refetch();
},
},
);
// Action: toggle a todo
export const toggleTodoAction = createAction(
(id: string) => mockApi.toggleTodo(id),
{
optimistic: (id) => {
// Immediately update the UI before the server responds
const current = todosResource() ?? [];
todosResource.mutate(
current.map(t => t.id === id ? { ...t, completed: !t.completed } : t)
);
},
revertOptimistic: () => {
// If the server call fails, refetch to restore correct state
todosResource.refetch();
},
onSuccess: () => {
todosResource.refetch();
},
},
);
// Action: delete a todo
export const deleteTodoAction = createAction(
(id: string) => mockApi.deleteTodo(id),
{
optimistic: (id) => {
const current = todosResource() ?? [];
todosResource.mutate(current.filter(t => t.id !== id));
},
revertOptimistic: () => {
todosResource.refetch();
},
onSuccess: () => {
todosResource.refetch();
},
},
);
// Action: edit a todo
export const editTodoAction = createAction(
({ id, text }: { id: string; text: string }) => mockApi.updateTodo(id, text),
{
onSuccess: () => {
todosResource.refetch();
},
},
);How createResource Works
const todosResource = createResource<Todo[]>(
() => mockApi.getTodos(), // Fetcher function
{ initialData: [], staleTime: 30_000 }
);
todosResource(); // Todo[] | undefined — the data
todosResource.loading(); // boolean — is currently fetching
todosResource.error(); // Error | undefined — last error
todosResource.refetch(); // Manually trigger a refetch
todosResource.mutate(v); // Optimistically set the dataThe resource automatically fetches when created and re-fetches when any reactive dependency inside the fetcher changes.
How createAction Works
const addTodoAction = createAction(
(text: string) => mockApi.addTodo(text), // Mutation function
{
onSuccess: (result, input) => { /* ... */ },
onError: (error, input) => { /* ... */ },
optimistic: (input) => { /* ... */ },
revertOptimistic: (input) => { /* ... */ },
}
);
await addTodoAction.execute('Buy milk'); // Execute the mutation
addTodoAction.loading(); // boolean
addTodoAction.error(); // Error | undefinedUpdate the Page to Use the Resource
Replace the local signal state in src/pages/AllTodos.akash:
<script>
import { computed } from '@akashjs/runtime';
import {
todosResource,
addTodoAction,
toggleTodoAction,
deleteTodoAction,
editTodoAction,
} from '../api/todos';
import AddTodoForm from '../components/AddTodoForm.akash';
import TodoList from '../components/TodoList.akash';
const todos = computed(() => todosResource() ?? []);
const totalCount = computed(() => todos().length);
const completedCount = computed(() => todos().filter(t => t.completed).length);
function handleAdd(text: string) {
addTodoAction.execute(text);
}
function handleToggle(id: string) {
toggleTodoAction.execute(id);
}
function handleDelete(id: string) {
deleteTodoAction.execute(id);
}
function handleEdit(id: string, text: string) {
editTodoAction.execute({ id, text });
}
</script>
<template>
<div>
<AddTodoForm onAdd={handleAdd} />
{#if todosResource.loading() && todos().length === 0}
<div class="loading">Loading todos...</div>
{:else if todosResource.error()}
<div class="error">
<p>Failed to load todos: {todosResource.error().message}</p>
<button on:click={() => todosResource.refetch()}>Retry</button>
</div>
{:else}
<p class="count">{totalCount()} total, {completedCount()} completed</p>
<TodoList
todos={todos()}
onToggle={handleToggle}
onDelete={handleDelete}
onEdit={handleEdit}
/>
{/if}
</div>
</template>
<style scoped>
.count {
font-size: 0.875rem;
color: var(--muted, #718096);
margin-bottom: 0.5rem;
}
.loading {
padding: 2rem;
text-align: center;
color: var(--muted, #718096);
}
.error {
padding: 1rem;
background: #fff5f5;
border: 1px solid var(--danger, #e53e3e);
border-radius: 8px;
color: var(--danger, #e53e3e);
text-align: center;
}
.error button {
margin-top: 0.5rem;
padding: 0.5rem 1rem;
background: var(--danger, #e53e3e);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>Notice how the loading and error states are handled with {#if} blocks. The todosResource.loading() signal is reactive, so the UI automatically switches between loading, error, and data states.
Optimistic Updates
Look at the toggle action again:
export const toggleTodoAction = createAction(
(id: string) => mockApi.toggleTodo(id),
{
optimistic: (id) => {
const current = todosResource() ?? [];
todosResource.mutate(
current.map(t => t.id === id ? { ...t, completed: !t.completed } : t)
);
},
revertOptimistic: () => {
todosResource.refetch();
},
},
);When you toggle a todo:
- Immediately:
optimisticruns and updates the UI viamutate() - In the background: The actual API call executes
- On success:
onSuccessrefetches to get the canonical server state - On failure:
revertOptimisticrefetches to undo the optimistic update
The user sees instant feedback while the real request happens asynchronously.
Retry and Error Handling
The @akashjs/http package includes a withRetry interceptor for automatic retries:
import { createHttpClient, withRetry } from '@akashjs/http';
const http = createHttpClient({
interceptors: [withRetry({ maxRetries: 3, delay: 1000 })],
});See the HTTP Guide for more on interceptors, auth headers, and error handling patterns.
Try It
Test the data fetching:
- Refresh the page and notice the brief "Loading todos..." message (simulated 300ms delay)
- Toggle a todo and notice how the checkbox updates instantly (optimistic update)
- Open the Network tab -- in a real app, you would see actual HTTP requests
- Try adding the
refetchOnFocus: trueoption tocreateResourceand switch tabs, then come back
Summary
You now know how to:
- Create an HTTP client with
createHttpClient - Fetch data reactively with
createResource(loading, error, refetch, mutate) - Perform mutations with
createAction(loading, error, lifecycle hooks) - Implement optimistic updates for instant UI feedback
- Handle loading and error states in templates
The data layer is working, but we still have duplicated state logic across pages. Let's fix that with a store.
What's Next: State Management -- extract todo logic into a shared global store.