Build a REST API: Create, Read, Update and Delete Tasks

Node.js and Express: Build a Back End API from Scratch · Express · 6 min read · Updated

CRUD on tasks

  1. A client uses four kinds of requests to work with tasks: create, read, change and delete.
  2. Each request goes to the same API address, and the method tells the API what to do.
  3. The API reads or changes the list of tasks. For now the list lives in memory.
  4. The API answers with a status code, such as 200, 201, 204 or 404, and usually the task as JSON.

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. Answer 200.
  • GET /api/tasks/:id: one task. Answer 200, or 404.
  • POST /api/tasks: create a task from the JSON body. Answer 201 with the new task.
  • PATCH /api/tasks/:id: change some fields of a task. Answer 200 with the updated task, or 404.
  • DELETE /api/tasks/:id: remove it. Answer 204, or 404.

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

  1. Create server.js with the CRUD routes and test all five routes with curl.
  2. Send a PATCH to a task that does not exist. Check the status code is 404.
  3. Add GET /api/tasks?done=false support to the list route (you did it in lesson 7).
  4. 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.

Frequently asked questions

What does CRUD mean?

Create, read, update and delete: the four basic jobs most apps do with data. In HTTP they are POST, GET, PATCH or PUT, and DELETE.

Why return 201 instead of 200 after creating something?

201 Created tells the client that a new resource was made. It is more precise than 200, and many tools and clients expect it.