Now you will put the pieces together. A REST API is a set of URLs where each URL stands for a thing, and the HTTP method says what to do with it. For the task list, the pattern is always the same: list, read one, create, update and delete. Programmers call these four jobs CRUD: create, read, update, delete. Almost every app you will ever build is CRUD with extra rules added on top.
The five routes
GET /api/tasks: list all tasks. Answer200.GET /api/tasks/:id: one task. Answer200, or404.POST /api/tasks: create a task from the JSON body. Answer201with the new task.PATCH /api/tasks/:id: change some fields of a task. Answer200with the updated task, or404.DELETE /api/tasks/:id: remove it. Answer204, or404.
We start with the data in a plain JavaScript array in memory. That keeps the code simple. The data disappears when the server stops, and we fix that in lesson 11. Create server.js like this.
import express from 'express';
const app = express();
app.use(express.json());
let tasks = [
{ id: 1, title: 'Learn Node', done: true },
{ id: 2, title: 'Learn Express', done: false },
];
let nextId = 3;
// Read all
app.get('/api/tasks', (req, res) => {
res.json(tasks);
});
// Read one
app.get('/api/tasks/:id', (req, res) => {
const task = tasks.find((t) => t.id === Number(req.params.id));
if (!task) return res.status(404).json({ error: 'Task not found' });
res.json(task);
});
// Create
app.post('/api/tasks', (req, res) => {
const task = { id: nextId++, title: req.body.title, done: false };
tasks.push(task);
res.status(201).json(task);
});
// Update part of a task
app.patch('/api/tasks/:id', (req, res) => {
const task = tasks.find((t) => t.id === Number(req.params.id));
if (!task) return res.status(404).json({ error: 'Task not found' });
if ('title' in req.body) task.title = req.body.title;
if ('done' in req.body) task.done = req.body.done;
res.json(task);
});
// Delete
app.delete('/api/tasks/:id', (req, res) => {
const before = tasks.length;
tasks = tasks.filter((t) => t.id !== Number(req.params.id));
if (tasks.length === before) return res.status(404).json({ error: 'Task not found' });
res.status(204).end();
});
app.listen(3000, () => console.log('Server running at http://localhost:3000'));
Read each handler and notice the pattern. For the routes with an id, we find the task in the array, and if it does not exist, we return a 404 straight away. Then we do the work. nextId++ gives each new task a new number, and the ++ adds one after the value is used. The delete route filters the task out and checks that the array became shorter, which tells us the task really existed.
Test every route with curl
curl -X POST http://localhost:3000/api/tasks -H "Content-Type: application/json" -d '{"title":"Ship it"}'
curl -X PATCH http://localhost:3000/api/tasks/3 -H "Content-Type: application/json" -d '{"done":true}'
curl -i -X DELETE http://localhost:3000/api/tasks/3
curl http://localhost:3000/api/tasks
{"id":3,"title":"Ship it","done":false}
{"id":3,"title":"Ship it","done":true}
HTTP/1.1 204 No Content
...
[{"id":1,"title":"Learn Node","done":true},{"id":2,"title":"Learn Express","done":false}]
Each call matches what you expect: the new task has id 3, the update marks it done, the delete answers with status 204 and no body, and the last list is back to the two original tasks. Doing this by hand is slow but valuable. It shows you exactly what a front end will send.
What is still wrong?
This API works for polite clients, but it trusts everything. Try sending an empty object to the create route.
curl -X POST http://localhost:3000/api/tasks -H "Content-Type: application/json" -d '{}'
{"id":4,"done":false}
The server created a task with no title. Worse, a client could send { "title": 12345 }, a huge text, or the wrong type for done, and PATCH would store it. Never trust data from outside your program. In the next lesson you will add validation and proper error handling. Other limits of this version: anyone can see and change every task, because there is no login yet, and everything is lost on restart.
PUT or PATCH?
Both change an existing thing. PUT means replace the whole thing with the data sent, so the client must send every field. PATCH means change only the fields that were sent. For task apps, where you might only tick a box, PATCH is the more practical one. Many APIs support both. We use PATCH.
Why return the new object?
After a create or an update, we send back the full task as the server now stores it. The client then has the id and any fields that the server added, so it does not need a second request. It is a small habit that makes front-end code much simpler.
Using the API from a front end
A React app can now call this API with fetch. For example, fetch('http://localhost:3000/api/tasks') returns the list. A browser blocks requests from one origin to another unless the server allows it. That rule is called CORS and we will configure it in lesson 14, together with the other security settings.
Your task
- Create
server.jswith the CRUD routes and test all five routes with curl. - Send a PATCH to a task that does not exist. Check the status code is 404.
- Add
GET /api/tasks?done=falsesupport to the list route (you did it in lesson 7). - Write down three kinds of bad data a client could send that this code would accept.
How to check it worked
- Create, update, delete and list all return the results shown above.
- The delete route returns 204 with no body, and a second delete of the same id returns 404.
- You listed at least three bad inputs the server does not reject yet.