Build a todo app before your backend exists
Turn a two-field schema into a hosted, persistent CRUD API, then connect it to a working React client. No database migrations, controllers, or deployment pipeline required.
A todo app is small, but its backend still needs real behavior: records must persist, updates must be validated, and filters must return the right data. APIMock's Data API gives you that behavior from a schema, so frontend work can start while the production API is still being designed.
Instant CRUD
Five generated routes from one resource.
Persistent state
Changes survive across requests and clients.
Repeatable demos
Reset live data back to known seeds.
Create the todos resource
Sign in to APIMock, open your workspace, and navigate to Endpoints from the sidebar. Click the Create resource button to open the creation wizard, and create a resource with the name todos.
Add these fields:
| Field | Type | Options |
|---|---|---|
| title | string | required |
| completed | boolean | required |
id, createdAt, and updatedAt automatically. IDs are sequential within the resource, so they are predictable in tests and easy to read in request logs.Give every run a useful starting point
Click on any of your new generated todos endpoints to open the editor. Select theData tab, toggle to Initial Data, and add two records. Initial records keep stable IDs and are restored whenever you reset the resource.
Record id: 1
{
"title": "Connect the API",
"completed": true
}
Record id: 2
{
"title": "Build the todo interface",
"completed": false
}This is useful beyond demos. A QA run can create, update, and delete anything it needs, then reset the resource to the same baseline before the next run.
CRUD Endpoints are automatically live
Your five generated CRUD endpoints are automatically live and organized inside a todosfolder in your endpoint tree. They are available under your workspace's base URL:
GET /todos List todos
POST /todos Create a todo
GET /todos/:id Fetch one todo
PUT /todos/:id Update a todo
DELETE /todos/:id Delete a todoClick the ... menu on the todos folder in the endpoint list and selectView OpenAPI Spec to copy your exact base URL and inspect or download the generated OpenAPI document. You can also import that document directly into Postman, Insomnia, or Swagger UI.
Connect the browser client
Add a small API module to your frontend and replace the placeholder host with the base URL shown in APIMock. CORS allows browser requests by default.
const API_URL = "https://<your-workspace>.<host>/todos";
export async function listTodos(show = "all") {
const filter = show === "all" ? "" : `&filter[completed]=${show === "done"}`;
const response = await fetch(`${API_URL}?limit=100${filter}`);
if (!response.ok) throw new Error("Could not load todos");
return (await response.json()).data;
}
export async function addTodo(title) {
const response = await fetch(API_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title, completed: false }),
});
if (!response.ok) throw new Error("Could not create todo");
return (await response.json()).data;
}
export async function toggleTodo(todo) {
const response = await fetch(`${API_URL}/${todo.id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ...todo, completed: !todo.completed }),
});
if (!response.ok) throw new Error("Could not update todo");
return (await response.json()).data;
}
export async function deleteTodo(id) {
const response = await fetch(`${API_URL}/${id}`, { method: "DELETE" });
if (!response.ok) throw new Error("Could not delete todo");
}Every successful write returns a { data } envelope. Lists return { data, meta }, where meta contains pagination totals. Errors have a stable { error: { code, message } } shape, so your UI can handle failures consistently.
createdAt cannot be passed to sort; declare your own date or position field if the interface needs explicit server-side ordering.Wire it into React
The component below loads todos, creates new records, toggles completion, deletes records, and lets the server filter the list. Styling is intentionally omitted so you can drop the behavior into your own design system.
import { useEffect, useState } from "react";
import { addTodo, deleteTodo, listTodos, toggleTodo } from "./api";
export default function TodoApp() {
const [todos, setTodos] = useState([]);
const [title, setTitle] = useState("");
const [filter, setFilter] = useState("all");
async function refresh(nextFilter = filter) {
setTodos(await listTodos(nextFilter));
}
useEffect(() => {
refresh();
}, [filter]);
async function submit(event) {
event.preventDefault();
if (!title.trim()) return;
await addTodo(title.trim());
setTitle("");
await refresh();
}
return (
<main>
<h1>Today</h1>
<form onSubmit={submit}>
<input
value={title}
onChange={(event) => setTitle(event.target.value)}
placeholder="What needs doing?"
/>
<button>Add</button>
</form>
<nav>
{["all", "open", "done"].map((value) => (
<button key={value} onClick={() => setFilter(value)}>
{value}
</button>
))}
</nav>
<ul>
{todos.map((todo) => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={async () => {
await toggleTodo(todo);
await refresh();
}}
/>
<span>{todo.title}</span>
<button onClick={async () => {
await deleteTodo(todo.id);
await refresh();
}}>
Delete
</button>
</li>
))}
</ul>
</main>
);
}Notice that the filter is not implemented with Array.filter in the browser. The client sends filter[completed]=true or filter[completed]=false, which exercises the same server-side request flow your production client will use.
Test the behavior, then reset
Open the app in two browser windows. Create a todo in one and refresh the other. The record is still there because resources are stateful—not a fixed response masquerading as CRUD.
- Create a todo and confirm the API assigns the next integer ID.
- Toggle it and verify updatedAt changes.
- Filter between open and completed records.
- Delete a record and confirm a later GET returns 404.
- Open the Inspector to review the requests your client sent.
When you are done, click the ... menu on the todos folder in the endpoint list and selectReset Data (or use the **Reset to initial data** button inside the **Data** tab of the endpoint editor). APIMock deletes the live records, restores IDs 1 and 2 from the seed, and rewinds the counter. Your next run starts clean.
Keep building
Your frontend now has a real API contract
Add priorities, due dates, or an enum status to the resource as the product evolves. When the production backend is ready, keep the contract and swap the base URL.