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
GETreads data. It must never change anything. Browsers and caches rely on that.POSTcreates something new, with the data in the body.PUTreplaces or changes something that exists. (PATCHis for partial changes; this course usesPUTfor both to keep the API small.)DELETEremoves 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
- Run the server with
node examples/05-http-rest/rest-without-express.js. - In a second terminal, run the four
curlcommands from the comment at the top, one by one. Use-iso you see the status line and headers. - Send a
POSTwith an empty body and confirm you get400and a message. - Add a
PUT /api/jobs/:idroute that changescompanyandrole, and returns404for an unknown id.
How to check it worked
GETreturns the list with status 200.- The
POSTreturns 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
GETto read,POSTto create,PUTto change,DELETEto 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.