Working with REST APIs
Learn how REST APIs map HTTP methods to CRUD operations, and build a component that fetches, creates, and deletes data cleanly.
Introduction
You now know how to fire off individual requests with fetch() and Axios. Real applications, however, rarely make just one type of request — they read lists of data, create new records, update existing ones, and delete records the user no longer wants. REST is the convention that most APIs follow to organize all of this.
This lesson ties together everything from the last two lessons into a single, realistic component: a task list that fetches items, adds a new one, and deletes one, all against a REST API, while keeping the code organized and the UI honest about loading and error states.
- What REST means at a practical, day-to-day level.
- How HTTP methods map to CRUD operations.
- How to build a component that reads, creates, and deletes data.
- How to handle loading, error, and success states cleanly.
- Why API calls should live in a separate module, not scattered across components.
What REST Means in Practice
REST (Representational State Transfer) is a set of conventions for designing web APIs around "resources" — things like users, posts, or tasks. Each resource typically has its own URL, and you interact with it using standard HTTP methods rather than inventing a new endpoint for every action.
For example, a "tasks" resource is usually reachable at a URL like /tasks, with individual tasks at /tasks/:id. What changes between reading, creating, updating, and deleting the same resource is not the URL structure so much as the HTTP method used.
Resources
Nouns like /tasks or /users represent the "things" your API exposes, not actions.
Predictable URLs
/tasks lists all tasks; /tasks/5 refers to a single task with id 5.
Methods, Not Verbs in URLs
You use GET, POST, PUT, DELETE instead of /getTasks or /deleteTask endpoints.
JSON Payloads
Most modern REST APIs send and receive data as JSON.
HTTP Methods and CRUD
CRUD stands for Create, Read, Update, Delete — the four basic operations you perform on any stored resource. REST APIs map each of these directly onto an HTTP method.
| CRUD Operation | HTTP Method | Example URL | Typical Use |
|---|---|---|---|
| Create | POST | /tasks | Add a brand-new task |
| Read (all) | GET | /tasks | Fetch the full list of tasks |
| Read (one) | GET | /tasks/5 | Fetch a single task by id |
| Update | PUT / PATCH | /tasks/5 | Modify an existing task |
| Delete | DELETE | /tasks/5 | Remove a task permanently |
PUT typically replaces the entire resource with the data you send, while PATCH updates only the specific fields provided. Many APIs use them interchangeably in practice, but PATCH is the more precise choice for partial updates.
Building the Data Layer
Scattering fetch or axios calls directly inside components makes them harder to test, harder to reuse, and harder to change later if the API shifts. A cleaner approach is to put all API calls for a resource into their own module, and have components simply call functions from it.
import axios from 'axios';
const api = axios.create({
baseURL: 'https://jsonplaceholder.typicode.com',
});
export function getTasks() {
return api.get('/todos?_limit=5').then((res) => res.data);
}
export function createTask(task) {
return api.post('/todos', task).then((res) => res.data);
}
export function deleteTask(id) {
return api.delete(`/todos/${id}`);
}Now any component can import getTasks, createTask, or deleteTask without knowing anything about URLs, headers, or Axios itself. If the API ever changes, you only update this one file.
Reading Data (GET)
Fetching the list happens once, when the component mounts, using useEffect and the getTasks function from the data layer.
import { useState, useEffect } from 'react';
import { getTasks, createTask, deleteTask } from './api/tasks';
function TaskList() {
const [tasks, setTasks] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
getTasks()
.then((data) => setTasks(data))
.catch((err) => setError(err.message))
.finally(() => setLoading(false));
}, []);Creating Data (POST)
To add a new task, call createTask with the new data, then update local state with the server's response so the UI reflects the change immediately.
const [title, setTitle] = useState('');
const [submitting, setSubmitting] = useState(false);
const handleAdd = (e) => {
e.preventDefault();
if (!title.trim()) return;
setSubmitting(true);
createTask({ title, completed: false })
.then((newTask) => {
setTasks((prev) => [...prev, newTask]);
setTitle('');
})
.catch((err) => setError(err.message))
.finally(() => setSubmitting(false));
};Deleting Data (DELETE)
Deleting follows the same pattern: call the API, and once it succeeds, remove the item from local state so the list updates without a full refetch.
const handleDelete = (id) => {
deleteTask(id)
.then(() => {
setTasks((prev) => prev.filter((task) => task.id !== id));
})
.catch((err) => setError(err.message));
};
if (loading) return <p>Loading tasks...</p>;
if (error) return <p>Error: {error}</p>;
return (
<div>
<form onSubmit={handleAdd}>
<input value={title} onChange={(e) => setTitle(e.target.value)} />
<button type="submit" disabled={submitting}>Add Task</button>
</form>
<ul>
{tasks.map((task) => (
<li key={task.id}>
{task.title}
<button onClick={() => handleDelete(task.id)}>Delete</button>
</li>
))}
</ul>
</div>
);
}
export default TaskList;[input box] [Add Task]
• Buy groceries [Delete]
• Finish homework [Delete]
• Read a book [Delete]Handling Loading, Error, and Success States
A common source of bugs is forgetting that a request can be in one of several states at once: nothing has loaded yet, it succeeded, or it failed. The example above tracks loading and error separately from the tasks array itself, so the UI can always show the right thing.
Every network request effectively has three outcomes to handle: still loading, failed with an error, or succeeded with data. Designing your state variables around these three outcomes from the start avoids confusing edge cases later.
Common Mistakes
- Writing raw axios or fetch calls directly inside components instead of a shared API module.
- Forgetting to reset the error state before starting a new request, so an old error lingers on screen.
- Updating local state optimistically without handling what happens if the request actually fails.
- Not disabling the submit button while a POST request is in flight, allowing duplicate submissions.
- Mixing up which HTTP method to use — for example, using GET to delete a resource.
Best Practices
- Keep all API calls for a resource in one dedicated module, like api/tasks.js.
- Give components simple function names to call — getTasks(), createTask() — hiding URLs and headers.
- Track loading, error, and data as separate, clearly named state variables.
- Update local state after a successful request rather than always refetching the entire list.
- Match your HTTP method to the actual operation: GET to read, POST to create, PUT/PATCH to update, DELETE to remove.
Frequently Asked Questions
Does every API follow REST conventions?
No. Many do, but some use GraphQL, RPC-style endpoints, or their own custom conventions. REST is simply the most common convention for simple resource-based APIs.
Why organize API calls into a separate file instead of writing them in the component?
It keeps components focused on rendering, makes the API calls reusable across multiple components, and means you only have to update one place if an endpoint changes.
Should I refetch the whole list after creating or deleting an item?
You can, but it is often more efficient and gives a snappier UI to update local state directly with the response you already have, as shown in this lesson.
What is the difference between PUT and PATCH again?
PUT is meant to replace an entire resource; PATCH is meant to update only specific fields. In practice many APIs are lenient about which one you use.
What happens if a DELETE request fails?
You should catch the error and show feedback to the user instead of silently removing the item from the UI, since the item may still exist on the server.
Key Takeaways
- REST organizes APIs around resources, accessed through predictable URLs.
- CRUD operations map to HTTP methods: POST creates, GET reads, PUT/PATCH updates, DELETE removes.
- A dedicated API module keeps components clean and calls reusable.
- Loading, error, and success states should be tracked explicitly for a trustworthy UI.
- Updating local state after a successful request avoids unnecessary refetching.
Summary
REST gives you a predictable vocabulary for working with any API: resources reached at consistent URLs, manipulated through standard HTTP methods. Combining that with a dedicated data-layer module and careful loading/error/success handling produces components that are both reliable and easy to read.
In this lesson, you learned REST conventions, mapped CRUD operations to HTTP methods, and built a task list component that reads, creates, and deletes data through a clean, reusable API module. Next, you will learn why hardcoding values like API URLs directly in your code is risky, and how environment variables solve that.
- You understand REST conventions and how CRUD maps to HTTP methods.
- You can build a component that reads, creates, and deletes data.
- You know how to organize API calls into a reusable module.
- You are ready to learn about environment variables.