Foundations roadmap

Designing a JSON API

Log in to save this

Saving keeps this in your list across devices. It's a free account — no card.

An API is a promise to whoever calls it: send me this, and I will answer with that. A good one is predictable, so a frontend developer can guess the next route without reading your code. This lesson builds a small tasks API and covers the decisions that come up in almost every project: route names, response shapes, input checks, status codes, and what happens when the same request arrives twice.

Resources and routes

Design around the things your app stores (resources) and let the HTTP method say what you are doing to them. For tasks:

  • GET /tasks lists tasks
  • POST /tasks creates a task
  • GET /tasks/:id reads one task
  • PUT /tasks/:id replaces a task (PATCH changes only some fields)
  • DELETE /tasks/:id deletes a task

Here :id is a placeholder, so GET /tasks/7 reads task 7. Avoid routes like /getAllTasks or /createTask: the verb already lives in the method, and plural nouns keep every resource consistent.

JSON in, JSON out

Requests and responses carry JSON bodies. Keep shapes consistent: a single task is always the same object, a list is always an array of those objects, and errors always look alike.

{ "id": 7, "title": "Buy milk", "done": false }
{ "error": "title is required" }

Consistency matters because the client parses whatever you send. If your framework answers some errors with an HTML error page, a client that calls res.json() crashes with "Unexpected token <", which hides the real problem.

Validate on the server

Your frontend form may check that the title is filled in, but anyone can call your API directly with curl or a script and send anything. So the server checks every input, every time, before using it:

app.post('/tasks', (req, res) => {
  const { title } = req.body;
  if (typeof title !== 'string' || title.trim() === '') {
    return res.status(400).json({ error: 'title is required' });
  }
  const task = { id: nextId++, title: title.trim(), done: false };
  tasks.push(task);
  res.status(201).json(task);
});

Frontend validation is for the user's convenience. Server validation is what actually protects your data.

The right status code

Status codes let the client react without reading your error text:

  • 200 for a successful read or update, with the data in the body.
  • 201 when something was created. Send the new object back, including its id, so the client can link to it or update it next.
  • 204 for a success with nothing to return, common for DELETE.
  • 400 when the input is invalid, with a message saying which field.
  • 404 when /tasks/999 does not exist. Not a 200 with null, and not a 500.
  • 500 only for real bugs on your side. If bad input causes a 500, you are missing a validation check.

Idempotency: the double-click problem

A request is idempotent if sending it twice leaves the server in the same state as sending it once. GET, PUT and DELETE are idempotent by design: deleting task 7 twice still leaves task 7 deleted (the second call may answer 404, but nothing else changes), and putting the same full task twice gives the same task.

POST is not. Two identical POST /tasks calls make two tasks. That is harmless for a to-do list and a disaster for POST /payments: a user double-clicks "Pay", or a slow network makes the app retry, and the card is charged twice.

Disabling the button after the first click helps, but it does not cover retries or two open tabs. The reliable fix is on the server: the client creates a unique idempotency key once per payment attempt, and the server refuses to process the same key twice.

app.post('/payments', async (req, res) => {
  const key = req.get('Idempotency-Key');
  const earlier = await findPaymentByKey(key);
  if (earlier) return res.status(200).json(earlier);
  const payment = await chargeCard(req.body, key);
  res.status(201).json(payment);
});

A common mistake

Treating PUT like PATCH. By the usual meaning, PUT replaces the whole resource, so a client that sends only { "done": true } to a replace-style PUT can wipe out the title. If you want partial updates, offer PATCH and document it. Try it: sketch the five routes for a notes resource, write the JSON for one note, and decide the status code for each success and each failure.

Resources

Curated resources for this node are on the way. Use what you already know how to search for, and check back soon.