HTTP, JSON and REST: Methods, Status Codes and Routes

MERN Stack Full Course: MongoDB, Express, React and Node.js · Foundations · 5 min read · Updated

Anatomy of a REST call

  1. A request has a method, a path, headers and sometimes a JSON body.
  2. The method says what to do: GET reads, POST creates, PATCH changes and DELETE removes.
  3. The server matches the method and the path to a route and runs its code.
  4. The reply carries a status code: 2xx means success, 4xx means the request was wrong, 5xx means the server failed.
  5. The body of the reply is JSON, and the client reads it.

An API is a set of agreements: which addresses exist, what you can send to each, and what you get back. Almost all web APIs today follow the same conventions, called REST, which sit on top of HTTP. If you learn those conventions once, every API you meet will feel familiar, and your own API will be easy for others (and for your React app) to use.

An HTTP request

A request has a method that says what you want to do, a path that says which thing you mean, headers that carry extra information, and sometimes a body that carries data. A response has a status code, headers and sometimes a body. The two are always a pair: one request, one response.

The methods

  • GET reads data. It must never change anything. Browsers and caches rely on that.
  • POST creates something new, with the data in the body.
  • PUT replaces or changes something that exists. (PATCH is for partial changes; this course uses PUT for both to keep the API small.)
  • DELETE removes something.

Status codes you will use

  • 200 OK: it worked. 201 Created: a new thing was made. 204 No Content: it worked and there is nothing to send back (usual for delete).
  • 400 Bad Request: the data you sent is wrong. 401 Unauthorized: we do not know who you are. 403 Forbidden: we know you, but you may not do this. 404 Not Found: no such thing. 409 Conflict: it clashes with something that exists, such as an email already registered. 413 Payload Too Large: a file or body is too big.
  • 500 Internal Server Error: a bug on our side. The first digit is the family: 2 means success, 4 means the client made a mistake, 5 means the server did.

Choosing the right code is part of good API design. A React app can then react correctly: show field errors on 400, send the person to the login page on 401, and show “not found” on 404.

JSON

JSON is the text format for the body. It looks like a JavaScript object, but keys must be in double quotes and there are no functions or comments. JSON.stringify(object) makes text; JSON.parse(text) makes an object. Send the header Content-Type: application/json so the receiver knows how to read the body.

REST: nouns in the path, verbs in the method

REST names things, not actions. The path says *what* (/api/jobs or /api/jobs/42), and the method says *what to do with it*. So you do not make a path like /api/getAllJobs or /api/deleteJob; you use GET /api/jobs and DELETE /api/jobs/42. Collections are plural nouns, and one item is the collection plus an id. This is the shape of the API you will build.

// Lesson 5: HTTP and REST with only Node.js, so you can see what Express will do for you later.
// Run:  node examples/05-http-rest/rest-without-express.js   then in a second terminal:
//   curl -i http://localhost:3000/api/jobs
//   curl -i -X POST http://localhost:3000/api/jobs -H "Content-Type: application/json" -d '{"company":"Acme","role":"Clerk"}'
//   curl -i http://localhost:3000/api/jobs/1
//   curl -i -X DELETE http://localhost:3000/api/jobs/1
import http from 'node:http';

let nextId = 2;
const jobs = [{ id: 1, company: 'Northwind Bank', role: 'Teller' }];

const send = (res, status, body) => {
  res.writeHead(status, { 'Content-Type': 'application/json' });
  res.end(body === undefined ? '' : JSON.stringify(body));
};

const readBody = (req) =>
  new Promise((resolve) => {
    let raw = '';
    req.on('data', (chunk) => (raw += chunk));
    req.on('end', () => { try { resolve(JSON.parse(raw || '{}')); } catch { resolve(null); } });
  });

http.createServer(async (req, res) => {
  const [path] = req.url.split('?');
  const match = /^\/api\/jobs\/(\d+)$/.exec(path);

  if (req.method === 'GET' && path === '/api/jobs') return send(res, 200, jobs);                       // 200 OK
  if (req.method === 'POST' && path === '/api/jobs') {
    const body = await readBody(req);
    if (!body || !body.company || !body.role) return send(res, 400, { message: 'company and role are required' }); // 400 Bad Request
    const job = { id: nextId++, company: body.company, role: body.role };
    jobs.push(job);
    return send(res, 201, job);                                                                          // 201 Created
  }
  if (match && req.method === 'GET') {
    const job = jobs.find((j) => j.id === Number(match[1]));
    return job ? send(res, 200, job) : send(res, 404, { message: 'Job not found' });                     // 404 Not Found
  }
  if (match && req.method === 'DELETE') {
    const i = jobs.findIndex((j) => j.id === Number(match[1]));
    if (i === -1) return send(res, 404, { message: 'Job not found' });
    jobs.splice(i, 1);
    return send(res, 204);                                                                               // 204 No Content
  }
  send(res, 404, { message: 'No such route' });
}).listen(3000, () => console.log('REST demo on http://localhost:3000'));

This file is a complete REST server using only Node.js. Read it twice. Notice how much code goes into reading the body, matching the path with a regular expression and setting the status and headers. Express (lesson 9) removes almost all of that work, and the logic of each route stays the same. Seeing the long way once makes you appreciate, and understand, what the library does.

Your task

  1. Run the server with node examples/05-http-rest/rest-without-express.js.
  2. In a second terminal, run the four curl commands from the comment at the top, one by one. Use -i so you see the status line and headers.
  3. Send a POST with an empty body and confirm you get 400 and a message.
  4. Add a PUT /api/jobs/:id route that changes company and role, and returns 404 for an unknown id.

How to check it worked

  • GET returns the list with status 200.
  • The POST returns 201 and the new job with an id.
  • Deleting twice returns 204 and then 404.

Summary

  • A request has a method, path, headers and maybe a body; a response has a status code and maybe a body.
  • Use GET to read, POST to create, PUT to change, DELETE to remove.
  • Status codes tell the client what happened: 2xx worked, 4xx your mistake, 5xx our bug.
  • REST: plural nouns in the path, the method says the action.

Download the solution (mern-lesson-05.zip)

Frequently asked questions

What is the difference between 401 and 403?

401 means the server does not know who you are (no or bad token). 403 means it knows you but you are not allowed to do that. In this course a job that belongs to someone else answers 404, so that we do not reveal that it exists.

Do I have to follow REST exactly?

No, it is a convention rather than a law. Following it makes your API predictable for other developers and tools, which is why we use it.

What is the difference between PUT and PATCH?

By the book, PUT replaces a whole item and PATCH changes only some fields. Many APIs, including the one in this course, use PUT for partial updates to keep things simple. What matters is that you document it and stay consistent.